الفرق بين المراجعتين لصفحة: «TypeScript/classes»

من موسوعة حسوب
إضافة الصّفحة
 
خطأ في كتابة خطأ
 
سطر 151: سطر 151:
// ليسا متوافقين
// ليسا متوافقين
</syntaxhighlight>
</syntaxhighlight>
في هذا المثال، لدينا الصنف ‎<code>Animal</code>‎ والصّنف ‎<code>Rhino</code>‎، مع كون الصنف ‎<code>Rhino</code>‎ صنفًا فرعيًّا من الصنف ‎<code>Animal</code>‎. لدينا كذلك صنف جديد باسم ‎<code>Employee</code>‎ مُشابهٍ للصنف ‎<code>Animal</code>‎ في شكله. نُنشئُ نسخًا من هذه الأصناف ونحاول إسنادها لبعضها البعض لنرى ما سيحدث. ولأنّ ‎<code>Animal</code>‎ و‎<code>Rhino</code>‎ يتشاركان في الجانب الخاص (private side) من شكلهما من نفس التصريح ‎<code>‎private‎ name:‎ string‎‎</code>‎ الموجود داخل الصنف ‎<code>Animal</code>‎، فهُما متوافقان. لكن الصنف ‎<code>Employee</code>‎ لا يوجد في نفس الحالة. ونحصل على خطئ عندما نحاول تعيين نسخة من الصنف ‎<code>Employee</code>‎ إلى نسخة من الصنف ‎<code>Animal</code>‎، يُخبرنا هذا الخطأ بأنّ النوعين ليسا متوافقين. ورغم أنّ للصنف ‎<code>Employee</code>‎ عنصرًا خاصًّا باسم ‎<code>name</code>‎، إلّا أنّ هذا العنصر ليس هو نفسه ذلك الموجود في الصنف ‎<code>Animal</code>‎.
في هذا المثال، لدينا الصنف ‎<code>Animal</code>‎ والصّنف ‎<code>Rhino</code>‎، مع كون الصنف ‎<code>Rhino</code>‎ صنفًا فرعيًّا من الصنف ‎<code>Animal</code>‎. لدينا كذلك صنف جديد باسم ‎<code>Employee</code>‎ مُشابهٍ للصنف ‎<code>Animal</code>‎ في شكله. نُنشئُ نسخًا من هذه الأصناف ونحاول إسنادها لبعضها البعض لنرى ما سيحدث. ولأنّ ‎<code>Animal</code>‎ و‎<code>Rhino</code>‎ يتشاركان في الجانب الخاص (private side) من شكلهما من نفس التصريح ‎<code>‎private‎ name:‎ string‎‎</code>‎ الموجود داخل الصنف ‎<code>Animal</code>‎، فهُما متوافقان. لكن الصنف ‎<code>Employee</code>‎ لا يوجد في نفس الحالة. ونحصل على خطأ عندما نحاول تعيين نسخة من الصنف ‎<code>Employee</code>‎ إلى نسخة من الصنف ‎<code>Animal</code>‎، يُخبرنا هذا الخطأ بأنّ النوعين ليسا متوافقين. ورغم أنّ للصنف ‎<code>Employee</code>‎ عنصرًا خاصًّا باسم ‎<code>name</code>‎، إلّا أنّ هذا العنصر ليس هو نفسه ذلك الموجود في الصنف ‎<code>Animal</code>‎.


===المُحدّد ‎<code>protected</code>‎ ===
===المُحدّد ‎<code>protected</code>‎ ===

المراجعة الحالية بتاريخ 13:05، 4 أغسطس 2018


مقدمة

تعتمد لغة JavaScript التقليدية على الدوال والوراثة المعتمدة على سلسلة Prototype لبناء مكونات قابلة لإعادة الاستعمال، وقد يجد بعض المبرمجين هذه الطريقة غريبة ومرهقة، خاصّة الذين ألِفوا البرمجة كائنيّة التوجه التي تعتمد على الأصناف التي ترث وظيفة (functionality) الأصناف الأساس (base classes) وتُبنَى فيها الكائنات من هذه الأصناف. بدايةً من الإصدار ECMAScript 2015 المعروف كذلك بالإصدار ECMAScript 6، يُمكن لمبرمجي JavaScript بناء التطبيقات باستخدام البرمجة كائنيّة التوجّه المعتمِدة على الأصناف. وتسمح TypeScript للمطورين باستعمال هذه التقنيات الآن، وتُترجِمها إلى لغة JavaScript تعمل على جميع المنصات المتصفحات المعروفة، دون الحاجة إلى انتظار دعم النسخة التالية من JavaScript.

الأصناف

لنلق نظرة على مثال صنف بسيط:

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");

يجب على البنية العامّة (syntax) أن تبدو مألوفة لك إن سبق وأن استعملت لغات مثل C#‎ أو Java. هنا نُصرّح عن صنف جديد باسم ‎Greeter‎. لهذا الصنف ثلاثة عناصر: خاصيّة باسم ‎greeting‎، ودالة بانيّة (constructor)، وتابع باسم ‎greet‎.

ستُلاحظ بأنّنا نستعمل السابقة ‎this.‎‎ للوصول إلى عناصر الصنف، وهذا يدلّ على أنّه وصول إلى العنصر (member access).

في السطر الأخير، نقوم ببناء نسخة من الصنف ‎Greeter‎ باستعمال الكلمة المفتاحية ‎new‎. هذا يستدعي الدالة البانيّة التي عرّفنا مسبقًا، ما يُنشئ كائنًا جديدًا على شكل الصنف ‎Greeter‎ مُنفّذًا الدالة البانيّة لتهيئته.

الوراثة

يُمكننا استعمال أنماط البرمجة كائنيّة التوجّه الشّائعة في لغة TypeScript. وأحد الأنماط الأساسية هو قابلية توسيع الأصناف لإنشاء أصناف جديدة باستخدام الوراثة (inheritance).

لنلقِ نظرةً على مثال بسيط:

class Animal {
    move(distanceInMeters: number = 0) {
        console.log(`Animal moved ${distanceInMeters}m.`);
    }
}

class Dog extends Animal {
    bark() {
        console.log('Woof! Woof!');
    }
}

const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

يُوضّح هذا المثال ميزة الوراثة الأكثر مبدئيّةً: وهي أن الأصناف ترث الخاصيات والتوابع من الأصناف الأساس. هنا، الصنف ‎Dog‎ صنفٌ مُشتقٌّ يَشتقُّ من الصنف الأساس ‎Animal‎ بالكلمة المفتاحية ‎extends‎. عادةً ما يُطلق على الأصناف المُشتقَّة اسم "الأصناف الفرعيّة (subclasses)"، والأصناف الأساس يُطلق عليها اسم "الأصناف العليا (superclasses)".

لأنّ الصنف ‎Dog‎ يوسّع وظيفة الصنف ‎Animal‎، فقد استطعنا إنشاء نسخة من الصنف ‎Dog‎ لها كلا التابعين ‎bark()‎‎ و‎move()‎‎.

لننتقل الآن إلى مثال أعقد:

class Animal {
    name: string;
    constructor(theName: string) { this.name = theName; }
    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

class Snake extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 5) {
        console.log("Slithering...");
        super.move(distanceInMeters);
    }
}

class Horse extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 45) {
        console.log("Galloping...");
        super.move(distanceInMeters);
    }
}

let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");

sam.move();
tom.move(34);

يُغطّي هذا المثال بعض الميزات الأخرى التي لم نذكرها مسبقًا. نستخدم مُجدّدًا الكلمة المفتاحية ‎extends‎ لإنشاء صنفين فرعيّين من الصنف ‎Animal‎، وهُما ‎Horse‎ و‎Snake‎.

من الاختلافات الموجودة بين المثال أعلاه والذي يسبقه هو أن كل صنف مشتقّ يجب أن تحتوي دالته البانية على استدعاء ‎super()‎‎ الذي يُنفّذ الدالة البانيّة الخاصّة بالصنف الأساس. إضافةً إلى ذلك، لا بدّ من استدعاء ‎super()‎‎ قبل الوصول إلى خاصية باستعمال ‎this‎‎ في داخل دالة بانية. هذه قاعدة مهمّة تقضي بها TypeScript.

يوضح هذا المثال كذلك كيفيّة تجاوز (override) توابع الصنف الأساس واستبدالها بتوابع مُخصّصة للصنف الفرعي. إذ يقوم كل من الصنف ‎Snake‎ والصنف ‎Horse‎ بإنشاء تابع باسم ‎move‎ يتجاور ويغطّي على التابع ‎move‎ الموجود في الصنف ‎Animal‎، ما يمنح للتابع وظيفة خاصّة في كل صنف. لاحظ أن ‎tom‎ مصرحٌ عنه على أنّه من النوع ‎Animal‎، ولأن قيمته هي ‎Horse‎، فاستدعاء ‎‎tom.‎move‎(34)‎‎ سيستدعي التابع المُتجاوِز الموجود في الصنف ‎Horse‎:

Slithering...
Sammy the Python moved 5m.
Galloping...
Tommy the Palomino moved 34m.

المحدّدات ‎public‎، و‎private‎، و‎protected

كل شيء عام (public) افتراضيّا

تمكنّا في الأمثلة أعلاه من الوصول بحريّة إلى العناصر التي صرّحنا عنها في البرامج في كل مكان منها. إن كانت لديك خبرة في التعامل مع الأصناف في اللغات الأخرى، فقد تلاحظ بأنّنا لم نحتج إلى استعمال الكلمة المفتاحيّة ‎public‎ في الأمثلة أعلاه لجعل العناصر عامة، إذ تتطلّب مثلًا لغة C#‎ أن تسبق الكلمة المفتاحيّةُ ‎public‎ العناصرَ بوضوح لكي تكون مرئيّة. أمّا في TypeScript، فجميع العناصر عامّة بشكل افتراضيّ.

لكنّك لا تزال تستطيع تعليم (mark) عنصر بالكلمة ‎public‎ صراحةً (explicitly). إذ كان يُمكن كتابة الصنف ‎Animal‎ السابق كما يلي:

class Animal {
    public name: string;
    public constructor(theName: string) { this.name = theName; }
    public move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

المُحدّد ‎private

عندما يُحدَّد عنصر بالمُحدِّد ‎private‎‎، فهذا يعني بأنّه عنصر خاصّ لا يُمكن الوصول إليه من خارج الصنف الذي يحتوي عليه. مثلًا:

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

new Animal("Cat").name;
// خطأ، العنصر
// 'name'
// عنصرٌ خاصّ

تعتمد لغة TypeScript على نظام أنواع هيكلي (structural type system). عندما نقارن بين نوعين مختلفين، فسنقول بأنّهما متوافقان إذا كانت أنواع جميع العناصر متوافقة، وذلك بغضّ النظر عن مصدر هاذين النوعين.

لكن عند المقارنة بين نوعين يحتويان على عناصر خاصّة (private) وعناصر محميّة (protected)، فإنّنا نتعامل مع هاذين النوعين على أنّهما مختلفان. ولكي يتوافق نوعان لدى أحدهما عنصر خاص، فعلى الآخر أن يحتوي على عنصر خاص تأصّل من نفس التصريح. ونفس المبدأ ينطبق على العناصر المحميّة.

بالمثال يتّضح المقال:

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

class Rhino extends Animal {
    constructor() { super("Rhino"); }
}

class Employee {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");

animal = rhino;
animal = employee; // Error
// خطأ النوعان
// 'Animal'و 'Employee'
// ليسا متوافقين

في هذا المثال، لدينا الصنف ‎Animal‎ والصّنف ‎Rhino‎، مع كون الصنف ‎Rhino‎ صنفًا فرعيًّا من الصنف ‎Animal‎. لدينا كذلك صنف جديد باسم ‎Employee‎ مُشابهٍ للصنف ‎Animal‎ في شكله. نُنشئُ نسخًا من هذه الأصناف ونحاول إسنادها لبعضها البعض لنرى ما سيحدث. ولأنّ ‎Animal‎ و‎Rhino‎ يتشاركان في الجانب الخاص (private side) من شكلهما من نفس التصريح ‎‎private‎ name:‎ string‎‎‎ الموجود داخل الصنف ‎Animal‎، فهُما متوافقان. لكن الصنف ‎Employee‎ لا يوجد في نفس الحالة. ونحصل على خطأ عندما نحاول تعيين نسخة من الصنف ‎Employee‎ إلى نسخة من الصنف ‎Animal‎، يُخبرنا هذا الخطأ بأنّ النوعين ليسا متوافقين. ورغم أنّ للصنف ‎Employee‎ عنصرًا خاصًّا باسم ‎name‎، إلّا أنّ هذا العنصر ليس هو نفسه ذلك الموجود في الصنف ‎Animal‎.

المُحدّد ‎protected

يتصرّف المُحدّد ‎protected‎ مثل المُحدّد ‎private‎ باستثناء أنّ العناصر التي يُصرّح عنها على أنّها محميّة بالمُحدّد ‎protected‎ قابلة للوصول إليها من الأصناف المشتقّة. مثلًا:

class Person {
    protected name: string;
    constructor(name: string) { this.name = name; }
}

class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name);
        this.department = department;
    }

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // خطأ

لاحظ أنّه رغم أنّنا لا نستطيع الوصول إلى العنصر ‎name‎ من خارج الصنف ‎Person‎، إلا أنّه لا يزال بإمكاننا استعمالها من داخل تابع نسخة (instance method) في الصنف ‎Employee‎ لأن الصنف ‎Employee‎ يرث من الصنف ‎Person‎.

يُمكن كذلك تعليم الدالة البانية بالمُحدّد ‎protected‎. هذا يعني بأنّ الصنف لا يُمكن أن يُهيّأ (instantiated) أي لا يمكن أن تُنشأ نسخة منه خارج الصنف الذي يحتويه، لكنّ توسيعه (أي الوراثة منه) ممكن. مثلًا:

class Person {
    protected name: string;
    protected constructor(theName: string) { this.name = theName; }
}

// يُمكن للصنف
// Employee
// أن يُوسِّعَ الصنف
// Person

class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name);
        this.department = department;
    }

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
let john = new Person("John"); // خطأ، لا يُمكن إنشاء نسخة من الصنف لأنّ دالّته البانيّة محميّة

محدد قابلية القراءة فقط (Readonly modifier)

يُمكنك تحديد خاصيةٍ على أنها قابلة للقراءة فقط باستعمال الكلمة المفتاحية ‎readonly‎. يجب على الخاصيات القابلة للقراءة فقط أن تُهيأ (initialized) عند التصريح عنها أو داخل الدالة البانيّة.

class Octopus {
    readonly name: string;
    readonly numberOfLegs: number = 8;
    constructor (theName: string) {
        this.name = theName;
    }
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // خطأ، الخاصية قابلة للقراءة فقط، لا يمكن إسناد قيمة لها

خاصيات المعاملات (Parameter properties)

في المثال أعلاه، توجّب علينا التصريح عن عنصر قابل للقراءة فقط باسم ‎name‎ ومعامل للدالة البانيّة باسم ‎theName‎ في الصنف ‎Octopus‎، وبعدها فورًا نُعيّن قيمة ‎theName‎ للخاصية ‎name‎. طريقة العمل هذه شائعة جدًا. لذا فخاصيات المعاملات ميّزة تسمح لنا بإنشاء وتهيئة عنصر في مكان واحد. ما يلي مراجعة للصنف ‎Octopus‎ باستخدام خاصية معامل:

class Octopus {
    readonly numberOfLegs: number = 8;
    constructor(readonly name: string) {
    }
}

لاحظ كيف أنّنا تخلّينا عن استخدام ‎theName‎ كليًّا واستعملنا العبارة المختصرة ‎‎readonly‎ name:‎ string‎ ‎‎ كمعامل للدالة البانية لإنشاء وتهيئة العنصر ‎name‎. وقد جمعنا التصريحات مع التعيين في مكان واحد.

يُصرَّح عن خاصيات المعاملات عبر سَبْقِ معاملات الدالة البانية بمُحدّد وصول أو الكلمة المفتاحية ‎readonly‎، أو كليهما معًا. استخدام ‎private‎ مع خاصية معامل يُصرّح عن عنصر خاص ويهيئُه؛ ونفس المبدأ ينطبق مع كل من ‎public‎، و‎protected‎، و‎readonly‎.

توابع الوصول (Accessors)

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

لنُحوِّل صنفًا بسيطًا ليستعمل الكلمتين المفتاحيتين ‎get‎ و‎set‎. أولًا، لنبدأ بمثال خالٍ من توابع الجلب وتوابع الضبط:

class Employee {
    fullName: string;
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    console.log(employee.fullName);
}

السماح للناس بضبط قيمة للخاصية ‎ fullName‎التي تُمثّل الاسم الكامل للموظّف يُسهّل المأمورية، إلا أن السماح للناس بتغيير الأسماء بهذه البساطة قد يجلب لنا المشاكل.

في النسخة أدناه، نتحقّق من أن هناك جملة سرية (المتغيّر ‎passcode‎) قبل السماح بتعديل بيانات الموظف. نقوم بهذا عبر استبدال الوصول المباشر (direct access) إلى الخاصية ‎fullName‎ بتابع ‎set‎ يتحقق من وجود الجملة السرية وصحّتها قبل تغيير الاسم. ونُضيف تابع ‎get‎ للسماح بالمثال السابق بالعمل كما كان:

let passcode = "secret passcode";

class Employee {
    private _fullName: string;

    get fullName(): string {
        return this._fullName;
    }

    set fullName(newName: string) {
        if (passcode && passcode == "secret passcode") {
            this._fullName = newName;
        }
        else {
            console.log("Error: Unauthorized update of employee!");
        }
    }
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    console.log(employee.fullName);
}

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

هناك بعض الأمور المتعلقة بتوابع الوصول لأخذها بعين الاعتبار:

  • تتطلّب توابع الوصول ضبطَ المُترجم (compiler) لإخراج شيفرة تدعم النسخة ECMAScript 5 من JavaScript أو النسخ الأحدث منها. وتخفيض المستوى إلى ECMAScript 3 لا يُمكن.
  • عند استعمال تابع الوصول ‎get‎ دون تابع الوصول ‎set‎ فهذا يعني بأن الخاصية قابلة للقراءة فقط ‎readonly‎ تلقائيًّا. هذا مُفيد عند توليد ملفّ ‎‎.d.ts‎‎ من شيفرتك، لأن مستخدمي الخاصية سيُلاحظون بأنهم لا يستطيعون تغيير قيمتها.

الخاصيات الساكنة (Static Properties)

إلى الآن، تحدثنا فقط عن عناصر النسخة في الصنف، وهي العناصر التي تظهر عندما يُهيّئ الكائن. لكن يُمكننا إنشاء عناصر ساكنة كذلك، وهي العناصر التي تكون ظاهرة على الصنف ذاته عوضًا عن ظهورها على النسخ. في هذا المثال، نستعمل الكلمة المفتاحية ‎static‎ على نقطة الأصل ‎origin‎، وذلك لأنها قيمة عامة لجميع الشبكات (grids). بحيث تصل كل نسخة إلى هذه القيمة عبر وضع اسم الصنف قبلها كسابقة. وكما نستعمل السابقة ‎this.‎ عند الوصول إلى بيانات النسخ، فإنّنا نستعمل السابقة ‎‎Grid.‎‎ للوصول إلى البيانات الساكنة.

class Grid {
    static origin = {x: 0, y: 0};
    calculateDistanceFromOrigin(point: {x: number; y: number;}) {
        let xDist = (point.x - Grid.origin.x);
        let yDist = (point.y - Grid.origin.y);
        return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
    }
    constructor (public scale: number) { }
}

let grid1 = new Grid(1.0);  // 1x scale
let grid2 = new Grid(5.0);  // 5x scale

console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));

الأصناف المجرَّدة (Abstract Classes)

الأصناف المجرَّدة هي أصناف أساس (base classes) قد تُشتَقّ منها أصناف أخرى. ويُمكن ألا تُهيّأ مباشرة. وعلى النقيض من الواجهات، يُمكن للأصناف المجرَّدة أن تحتوي على تفاصيل تطبيق (implementation details) لعناصرها. تُستعمل الكلمة المفتاحية ‎abstract‎ لتعريف الأصناف المُجرّدة وتعريف التوابع المجرّدة داخل صنف مجرّد:

abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log("roaming the earth...");
    }
}

التوابع المجردة الموجودة داخل صنف مُجرد لا تحتوي أي تطبيق (implementation) ويجب أن تُطبَّق على الصنف المشتَق. بنية التوابع المجردة مشابهة لتوابع الواجهات، إذ كلاهما يُعرِّف توقيع التابع دون جسمه (أي الشيفرة التي تكون داخل التابع). لكن على التوابع المجرّدة أن تُسبَق بالكلمة المفتاحية ‎abstract‎ ويُمكن أن تحتوي على محدّدات وصول (access modifiers) اختياريًّا:

abstract class Department {

    constructor(public name: string) {
    }

    printName(): void {
        console.log("Department name: " + this.name);
    }

    abstract printMeeting(): void; // يجب تطبيقها في الأصناف المشتقّة
}

class AccountingDepartment extends Department {

    constructor() {
        // يجب على الدوال البانيّة داخل الأصناف المشتقّة استدعاء الدالة
        // super()
        super("Accounting and Auditing");
    }

    printMeeting(): void {
        console.log("The Accounting Department meets each Monday at 10am.");
    }

    generateReports(): void {
        console.log("Generating accounting reports...");
    }
}

let department: Department; // إنشاء مرجع يشير إلى نوع مجرَّدٍ أمرٌ مسموح به
department = new Department(); // خطأ، لا يمكن إنشاء نسخة من صنف مجرّد
department = new AccountingDepartment(); // إنشاء نسخة من صنف فرعي غير مجرّد وإسنادها إلى متغيّر مسموح به
department.printName();
department.printMeeting();
department.generateReports(); // خطأ، التابع غير موجود في التصريح عن النوع المجرّد

تقنيات متقدمة

الدوال البانية (Constructor functions)

عند التصريح عن صنف في TypeScript، فإنّك في الواقع تُنشئ عدة تصريحات في نفس الوقت. الأول هو نوع نُسخةِ الصّنف (the instance of the class).

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter: Greeter;
greeter = new Greeter("world");
console.log(greeter.greet());

نستعمل في التصريح ‎‎let greeter: Greeter‎‎ في المثال أعلاه الصنف ‎Greeter‎ ليكون نوع نسخ الصنف ‎Greeter‎. هذا الأمر مألوف جدا للمبرمجين المعتادين على لغات أخرى كائنيّة التوجه.

وننشئ كذلك قيمة أخرى نُسميها بالدالة البانيّة. هذه الدالة هي التي تُستدعَى عند إنشاء نُسخ من الصنف بالكلمة المفتاحية ‎new‎. لنُلقِ نظرة على شيفرة JavaScript التي يُولدها المثال أعلاه لمعرفة كيف يعمل هذا عمليًّا:

let Greeter = (function () {
    function Greeter(message) {
        this.greeting = message;
    }
    Greeter.prototype.greet = function () {
        return "Hello, " + this.greeting;
    };
    return Greeter;
})();

let greeter;
greeter = new Greeter("world");
console.log(greeter.greet());

سيُعيَّن التصريح ‎let Greeter‎ هنا للدالة البانية. نحصل على نسخة من الصنف عند استدعاء هذه الدالة وتنفيذها بالكلمة المفتاحية ‎new‎. تحتوي الدالة البانية كذلك على جميع عناصر الصنف الساكنة. يُمكن النظر إلى كل صنف على أنّ له جانبَ نسخة (instance side) وجانبًا ساكنًا (static side).

لنُعدّل المثال قليلًا لإظهار الفرق:

class Greeter {
    static standardGreeting = "Hello, there";
    greeting: string;
    greet() {
        if (this.greeting) {
            return "Hello, " + this.greeting;
        }
        else {
            return Greeter.standardGreeting;
        }
    }
}

let greeter1: Greeter;
greeter1 = new Greeter();
console.log(greeter1.greet());

let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";

let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet());

في هذا المثال، يعمل ‎greeter1‎ كما سبق. إذ نُنشئ نسخة من الصنف ‎Greeter‎، ونستعمل هذا الكائن. وقد رأينا هذا من قبل.

ثمّ بعدها نستعمل الصنف مباشرة. وننشئ هنا متغيّرًا جديدًا باسم ‎greeterMaker‎. سيحمل هذا المتغيّر الصنف نفسه، أو بالأحرى، دالّتَه البانيّة. نستعمل هنا ‎typeof Greeter‎ وكأنّنا نقول "أعطني نوع الصنف ‎Greeter‎ نفسه" عوضًا عن نوع النسخة. أو بدقّة أكثر، وكأنّنا نقول "أعطني نوع الرمز (symbol) المُسمّى بالاسم ‎Greeter‎" وهو نوع الدالة البانيّة. يحتوي هذا النوع على جميع العناصر الساكنة للصنف ‎Greeter‎ إضافة إلى الدالة البانيّة التي تُنشئ نسخًا من الصنف ‎Greeter‎. ونُظهر هذا عبر استخدام الكلمة المفتاحية ‎new‎ على ‎greeterMaker‎، مُنشئين نسخًا جديدة من ‎Greeter‎ مع استعمالها كما سبق.

استعمال صنف كواجهة (interface)

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

class Point {
    x: number;
    y: number;
}

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};

مصادر