مقدمة عن البرمجة الشيئية في JavaScript: الكائنات والنماذج الأولية والفئات

في العديد من لغات البرمجة ، تعتبر الفصول الدراسية مفهومًا محددًا جيدًا. في JavaScript ليس هذا هو الحال. أو على الأقل لم يكن هذا هو الحال. إذا كنت تبحث عن OOP و JavaScript ، فستواجه العديد من المقالات مع الكثير من الوصفات المختلفة حول كيفية محاكاة classJavaScript.

هل هناك طريقة KISS بسيطة لتعريف فئة في JavaScript؟ وإذا كان الأمر كذلك ، فلماذا العديد من الوصفات المختلفة لتحديد فئة؟

قبل الإجابة على هذه الأسئلة ، دعنا نفهم بشكل أفضل ماهية JavaScript Object.

كائنات في JavaScript

لنبدأ بمثال بسيط للغاية:

const a = {}; a.foo = 'bar';

في مقتطف الشفرة أعلاه ، يتم إنشاء كائن وتعزيزه بخاصية foo. إن إمكانية إضافة أشياء إلى كائن موجود هو ما يجعل JavaScript مختلفًا عن اللغات الكلاسيكية مثل Java.

بمزيد من التفصيل ، حقيقة أن الكائن يمكن تحسينه يجعل من الممكن إنشاء مثيل لفئة "ضمنية" دون الحاجة إلى إنشاء الفئة بالفعل. دعنا نوضح هذا المفهوم بمثال:

function distance(p1, p2) { return Math.sqrt( (p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2 ); } distance({x:1,y:1},{x:2,y:2});

في المثال أعلاه، لم أكن بحاجة إلى الدرجة نقطة لإنشاء نقطة، أنا فقط بمد مثيل Objectمضيفا xو yالممتلكات. مسافة الوظيفة لا تهتم إذا كانت الوسيطات هي مثيل للفئة Pointأم لا. حتى استدعاء distanceوظيفة مع اثنين من الكائنات التي لديها xو yالممتلكات من نوع Number، وسوف تعمل بشكل جيد فقط. يسمى هذا المفهوم أحيانًا بكتابة البطة .

حتى الآن ، لم أستخدم سوى كائن بيانات: كائن يحتوي على بيانات فقط ولا يحتوي على وظائف. لكن في JavaScript ، من الممكن إضافة وظائف إلى كائن:

const point1 = { x: 1, y: 1, toString() { return `(${this.x},${this.y})`; } }; const point2 = { x: 2, y: 2, toString() { return `(${this.x},${this.y})`; } };

هذه المرة ، الكائنات التي تمثل نقطة ثنائية الأبعاد لها toString()طريقة. في المثال أعلاه ، toStringتم تكرار الرمز ، وهذا ليس جيدًا.

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

نحن الآن جاهزون لتقديم الدروس.

دروس في JavaScript

ما هو الفصل؟ من القاموس: الفئة هي "مجموعة أو فئة من الأشياء التي تشترك في بعض الخصائص أو السمات وتختلف عن الآخرين حسب النوع أو النوع أو الجودة."

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

نظرًا لأنه يمكن تحسين الكائنات ، كما رأينا سابقًا ، فقد تكون هناك طرق لإنشاء طرق وخصائص مشاركة الكائنات. لكننا نريد أبسط واحد.

لحسن الحظ ، يوفر ECMAScript 6 الكلمة الأساسية class، مما يجعل من السهل جدًا إنشاء فصل دراسي:

class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return `(${this.x},${this.y})`; } }

لذلك ، في رأيي ، هذه هي أفضل طريقة للإعلان عن الفئات في JavaScript. غالبًا ما ترتبط الفئات بالميراث:

class Point extends HasXY { constructor(x, y) { super(x, y); } toString() { return `(${this.x},${this.y})`; } }

كما ترى في المثال أعلاه ، يكفي لتوسيع فئة أخرى استخدام الكلمة الأساسية extends.

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

const p = new Point(1,1); console.log(p instanceof Point); // prints true

يجب أن توفر الطريقة الجيدة الموجهة للكائنات لتحديد الفئات:

  • صيغة بسيطة للإعلان عن فئة
  • طريقة بسيطة للوصول إلى المثال الحالي ، ويعرف أيضًا باسم this
  • بناء جملة بسيط لتوسيع الفصل
  • طريقة بسيطة للوصول إلى مثيل الطبقة الفائقة ، ويعرف أيضًا باسم super
  • ربما ، طريقة بسيطة لمعرفة ما إذا كان الكائن هو مثيل لفئة معينة. obj instanceof AClassيجب أن يعود trueإذا كان هذا الكائن هو مثيل لتلك الفئة.

classيوفر بناء الجملة الجديد جميع النقاط أعلاه.

قبل إدخال classالكلمة المفتاحية ، كيف كانت طريقة تعريف فئة في JavaScript؟

بالإضافة إلى ذلك ، ما هو حقًا فئة في JavaScript؟ لماذا نتحدث كثيرًا عن النماذج الأولية ؟

الفصول في JavaScript 5

من صفحة Mozilla MDN حول الفئات:

فئات JavaScript ، التي تم تقديمها في ECMAScript 2015 ، هي في الأساس سكر نحوي على الميراث القائم على النموذج الأولي لـ JavaScript . لا يقدم بناء الجملة نموذج وراثة كائني جديد لجافا سكريبت.

المفهوم الرئيسي هنا هو الوراثة القائمة على النموذج الأولي . نظرًا لوجود الكثير من سوء الفهم حول ماهية هذا النوع من الميراث ، فسأنتقل خطوة بخطوة ، منتقلًا من classكلمة رئيسية إلى functionكلمة رئيسية.

class Shape {} console.log(typeof Shape); // prints function

ويبدو أن classو functionترتبط. هو classمجرد اسم مستعار ل function؟ لا ليس كذلك.

Shape(2); // Uncaught TypeError: Class constructor Shape cannot be invoked without 'new'

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

var Shape = function Shape() {} // Or just function Shape(){} var aShape = new Shape(); console.log(aShape instanceof Shape); // prints true

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

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

دعنا ننتقل ونكتشف ما هو النموذج الأولي :

class Shape { getName() { return 'Shape'; } } console.log(Shape.prototype.getName); // prints function getName() ...

في كل مرة تعلن فيها عن طريقة داخل فئة ، فإنك تضيف هذه الطريقة بالفعل إلى النموذج الأولي للوظيفة المقابلة. المكافئ في JS 5 هو:

function Shape() {} Shape.prototype.getName = function getName() { return 'Shape'; }; console.log(new Shape().getName()); // prints Shape

Sometimes the class-functions are called constructors because they act like constructors in a regular class.

You may wonder what happens if you declare a static method:

class Point { static distance(p1, p2) { // ... } } console.log(Point.distance); // prints function distance console.log(Point.prototype.distance); // prints undefined

Since static methods are in a 1 to 1 relation with classes, the static function is added to the constructor-function, not to the prototype.

Let’s recap all these concepts in a simple example:

function Point(x, y) { this.x = x; this.y = y; } Point.prototype.toString = function toString() { return '(' + this.x + ',' + this.y + ')'; }; Point.distance = function distance() { // ... } console.log(new Point(1,2).toString()); // prints (1,2) console.log(new Point(1,2) instanceof Point); // prints true

Up to now, we have found a simple way to:

  • declare a function that acts as a class
  • access the class instance using the this keyword
  • create objects that are actually an instance of that class (new Point(1,2) instanceof Point returns true )

But what about inheritance? What about accessing the super class?

class Hello { constructor(greeting) { this._greeting = greeting; } greeting() { return this._greeting; } } class World extends Hello { constructor() { super('hello'); } worldGreeting() { return super.greeting() + ' world'; } } console.log(new World().greeting()); // Prints hello console.log(new World().worldGreeting()); // Prints hello world

Above is a simple example of inheritance using ECMAScript 6, below the same example using the the so called prototype inheritance:

function Hello(greeting) { this._greeting = greeting; } Hello.prototype.greeting = function () { return this._greeting; }; function World() { Hello.call(this, 'hello'); } // Copies the super prototype World.prototype = Object.create(Hello.prototype); // Makes constructor property reference the sub class World.prototype.constructor = World; World.prototype.worldGreeting = function () { const hello = Hello.prototype.greeting.call(this); return hello + ' world'; }; console.log(new World().greeting()); // Prints hello console.log(new World().worldGreeting()); // Prints hello world

This way of declaring classes is also suggested in the Mozilla MDN example here.

Using the class syntax, we deduced that creating classes involves altering the prototype of a function. But why is that so? To answer this question we must understand what the new operator actually does.

New operator in JavaScript

The new operator is explained quite well in the Mozilla MDN page here. But I can provide you with a relatively simple example that emulates what the new operator does:

function customNew(constructor, ...args) { const obj = Object.create(constructor.prototype); const result = constructor.call(obj, ...args); return result instanceof Object ? result : obj; } function Point() {} console.log(customNew(Point) instanceof Point); // prints true

Note that the real new algorithm is more complex. The purpose of the example above is just to explain what happens when you use the new operator.

When you write new Point(1,2)what happens is:

  • The Point prototype is used to create an object.
  • The function constructor is called and the just created object is passed as the context (a.k.a. this) along with the other arguments.
  • If the constructor returns an Object, then this object is the result of the new, otherwise the object created from the prototype is the result.

So, what does prototype inheritance mean? It means that you can create objects that inherit all the properties defined in the prototype of the function that was called with the new operator.

If you think of it, in a classical language the same process happens: when you create an instance of a class, that instance can use the this keyword to access to all the functions and properties (public) defined in the class (and the ancestors). As opposite to properties, all the instances of a class will likely share the same references to the class methods, because there is no need to duplicate the method’s binary code.

Functional programming

Sometimes people say that JavaScript is not well suited for Object Oriented programming, and you should use functional programming instead.

بينما لا أوافق على أن JS ليست مناسبة لـ OOP ، أعتقد أن البرمجة الوظيفية هي طريقة جيدة جدًا للبرمجة. وظائف JavaScript هي مواطنين من الدرجة الأولى (على سبيل المثال ، يمكنك تمرير وظيفة إلى وظيفة أخرى) وتوفر ميزات مثل bind، callأو applyالتي هي بنيات أساسية تستخدم في البرمجة الوظيفية.

بالإضافة إلى ذلك ، يمكن اعتبار برمجة RX بمثابة تطور (أو تخصص) للبرمجة الوظيفية. الق نظرة على RxJs هنا.

استنتاج

استخدم ، إن أمكن ، classبناء جملة ECMAScript 6 :

class Point { toString() { //... } }

أو استخدم نماذج الوظائف لتحديد الفئات في ECMAScript 5:

function Point() {} Point.prototype.toString = function toString() { // ... }

أتمنى أن تكون قد استمتعت بالقراءة!