de Andrea Koutifaris

O introducere în programarea orientată obiect în JavaScript: obiecte, prototipuri și clase

O introducere in programarea orientata obiect in JavaScript obiecte prototipuri
Obiecte într-o clasă (cameră).

În multe limbaje de programare, clasele sunt un concept bine definit. În JavaScript nu este cazul. Sau cel puțin nu a fost cazul. Dacă căutați OOP și JavaScript, veți întâlni multe articole cu o mulțime de rețete diferite despre cum puteți emula un class în JavaScript.

Există o modalitate simplă, KISS de a defini o clasă în JavaScript? Și dacă da, de ce atât de multe rețete diferite pentru a defini o clasă?

Înainte de a răspunde la aceste întrebări, să înțelegem mai bine ce este un JavaScript Object este.

Obiecte în JavaScript

Să începem cu un exemplu foarte simplu:

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

În fragmentul de cod de mai sus, un obiect este creat și îmbunătățit cu o proprietate foo. Posibilitatea de a adăuga lucruri la un obiect existent face ca JavaScript să fie diferit de limbajele clasice precum Java.

Mai detaliat, faptul că un obiect poate fi îmbunătățit face posibilă crearea unei instanțe a unei clase „implicite” fără a fi necesară crearea clasei. Să clarificăm acest concept cu un exemplu:

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});

În exemplul de mai sus, nu aveam nevoie de o clasă Point pentru a crea un punct, ci doar am extins o instanță de Object adăugând x și y proprietăți. Funcției distanță nu îi pasă dacă argumentele sunt o instanță a clasei Point sau nu. Până să suni distance funcționează cu două obiecte care au un x și y proprietate de tip Number, va funcționa foarte bine. Acest concept este uneori numit tastarea rațelor.

Până în prezent, am folosit doar un obiect de date: un obiect care conține doar date și nu are funcții. Dar în JavaScript este posibil să adăugați funcții unui obiect:

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

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

De data aceasta, obiectele care reprezintă un punct 2D au un toString() metodă. În exemplul de mai sus, toString codul a fost duplicat, iar acest lucru nu este bun.

Există multe modalități de a evita această duplicare și, de fapt, în diferite articole despre obiecte și clase din JS veți găsi soluții diferite. Ați auzit vreodată de „modelul modulului revelator”? Conține cuvintele „model” și „revelator”, sună bine, iar „modul” este o necesitate. Deci, trebuie să fie modul corect de a crea obiecte … cu excepția faptului că nu este. Modelul modulului revelator poate fi alegerea corectă în unele cazuri, dar cu siguranță nu este modalitatea implicită de a crea obiecte cu comportamente.

Acum suntem gata să introducem cursuri.

Cursuri în JavaScript

Ce este o clasă? Dintr-un dicționar: o clasă este „un set sau categorie de lucruri care au o proprietate sau un atribut în comun și diferențiate de altele după fel, tip sau calitate”.

În limbajele de programare spunem adesea „Un obiect este o instanță a unei clase”. Aceasta înseamnă că, folosind o clasă, pot crea multe obiecte și toate împărtășesc metode și proprietăți.

Deoarece obiectele pot fi îmbunătățite, așa cum am văzut mai devreme, există mai multe modalități de a crea metode și proprietăți de partajare a obiectelor. Dar vrem pe cel mai simplu.

Din fericire ECMAScript 6 oferă cuvântul cheie class, facilitând crearea unei clase:

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return `(${this.x},${this.y})`;
  }
}

Deci, în opinia mea, acesta este cel mai bun mod de declarare a cursurilor în JavaScript. Clasele sunt adesea legate de moștenire:

class Point extends HasXY {
  constructor(x, y) {
    super(x, y);
  }

  toString() {
    return `(${this.x},${this.y})`;
  }
}

După cum puteți vedea în exemplul de mai sus, pentru a extinde o altă clasă este suficient să folosiți cuvântul cheie extends .

Puteți crea un obiect dintr-o clasă folosind new operator:

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

Un mod bun de definire a orelor orientat pe obiecte ar trebui să ofere:

  • o sintaxă simplă pentru a declara o clasă
  • un mod simplu de a accesa instanța curentă, aka this
  • o sintaxă simplă pentru a extinde o clasă
  • un mod simplu de a accesa instanța super clasă, aka super
  • eventual, un mod simplu de a spune dacă un obiect este o instanță a unei anumite clase. obj instanceof AClass ar trebui să se întoarcă true dacă acel obiect este o instanță a acelei clase.

Noul class sintaxa oferă toate punctele de mai sus.

Înainte de introducerea class cuvânt cheie, care a fost modalitatea de a defini o clasă în JavaScript?

În plus, ce este cu adevărat o clasă în JavaScript? De ce vorbim des despre asta prototipuri?

Cursuri în JavaScript 5

Din Pagina Mozilla MDN despre cursuri:

Clasele JavaScript, introduse în ECMAScript 2015, sunt în principal zahăr sintactic decât cele existente în JavaScript moștenirea bazată pe prototip. Sintaxa clasei nu introduce un nou model de moștenire orientat obiect în JavaScript.

Conceptul cheie aici este moștenirea bazată pe prototip. Deoarece există o mulțime de neînțelegeri cu privire la ceea ce este acest tip de moștenire, voi proceda pas cu pas, trecând de la class cuvânt cheie către function cuvânt cheie.

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

Se pare ca class și function sunt inruditi. Este class doar un alias pentru function ? Nu, nu este.

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

Deci, se pare că oamenii care au introdus class cuvântul cheie a vrut să ne spună că o clasă este o funcție care trebuie numită folosind new operator.

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

Exemplul de mai sus arată că putem folosi function a declara o clasă. Cu toate acestea, nu putem forța utilizatorul să apeleze funcția folosind new operator. Este posibil să aruncați o excepție dacă new operatorul nu a fost folosit pentru a apela funcția.

Oricum vă sugerez să nu puneți această verificare în fiecare funcție care acționează ca o clasă. În schimb, utilizați această convenție: orice funcție al cărei nume începe cu o literă mare este o clasă și trebuie apelată folosind new operator.

Să mergem mai departe și să aflăm ce a prototip este:

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

De fiecare dată când declarați o metodă în cadrul unei clase, adăugați de fapt acea metodă la prototipul funcției corespunzătoare. Echivalentul din JS 5 este:

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

Uneori funcțiile de clasă sunt numite constructori deoarece se comportă ca constructori într-o clasă obișnuită.

S-ar putea să vă întrebați ce se întâmplă dacă declarați o metodă statică:

class Point {
  static distance(p1, p2) {
    // ...
  }
}

console.log(Point.distance); // prints function distance
console.log(Point.prototype.distance); // prints undefined

Deoarece metodele statice sunt într-o relație de la 1 la 1 cu clasele, funcția statică este adăugată la funcția constructor, nu la prototip.

Să recapitulăm toate aceste concepte într-un exemplu simplu:

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

Până în prezent, am găsit o modalitate simplă de a:

  • declarați o funcție care acționează ca o clasă
  • accesați instanța clasei folosind this cuvânt cheie
  • creați obiecte care sunt de fapt o instanță a acelei clase (new Point(1,2) instanceof Point se intoarce true )

Dar ce zici de moștenire? Dar accesarea super clasei?

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

Mai sus este un exemplu simplu de moștenire folosind ECMAScript 6, sub același exemplu folosind așa-numitul moștenirea prototipului:

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

Acest mod de declarare a claselor este sugerat și în exemplul Mozilla MDN Aici.

Folosind class sintaxa, am dedus că crearea de clase implică modificarea prototipului unei funcții. Dar de ce este așa? Pentru a răspunde la această întrebare trebuie să înțelegem ce new operatorul de fapt.

Operator nou în JavaScript

new operatorul este explicat destul de bine în pagina Mozilla MDN Aici. Dar vă pot oferi un exemplu relativ simplu care emulează ceea ce new operatorul face:

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

Rețineți că realul new algoritmul este mai complex. Scopul exemplului de mai sus este doar de a explica ce se întâmplă atunci când utilizați new operator.

Când scrii new Point(1,2)ceea ce se întâmplă este:

  • Point prototipul este folosit pentru a crea un obiect.
  • Constructorul de funcții este apelat și obiectul creat tocmai este trecut ca context (alias this) împreună cu celelalte argumente.
  • Dacă constructorul returnează un obiect, atunci acest obiect este rezultatul noului, altfel rezultatul este obiectul creat din prototip.

Deci, ce face moștenirea prototipului Rău? Înseamnă că puteți crea obiecte care moștenesc toate proprietățile definite în prototipul funcției care a fost apelată cu new operator.

Dacă vă gândiți la asta, într-un limbaj clasic se întâmplă același proces: atunci când creați o instanță a unei clase, acea instanță poate folosi this cuvânt cheie pentru a accesa toate funcțiile și proprietățile (publice) definite în clasă (și strămoșii). Spre deosebire de proprietăți, toate instanțele unei clase vor împărtăși probabil aceleași referințe la metodele clasei, deoarece nu este nevoie să duplicați codul binar al metodei.

Programare funcțională

Uneori oamenii spun că JavaScript nu este potrivit pentru programarea orientată pe obiecte și ar trebui să utilizați în schimb programarea funcțională.

Deși nu sunt de acord că JS nu este potrivit pentru OOP, cred că programarea funcțională este un mod foarte bun de programare. În JavaScript funcțiile sunt cetățeni de primă clasă (de exemplu, puteți trece o funcție către o altă funcție) și oferă caracteristici precum bind , call sau apply care sunt construcții de bază utilizate în programarea funcțională.

În plus, programarea RX ar putea fi văzută ca o evoluție (sau o specializare) a programării funcționale. Uită-te la RxJs aici.

Concluzie

Folosiți, atunci când este posibil, ECMAScript 6 class sintaxă:

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

sau utilizați prototipuri de funcții pentru a defini clase în ECMAScript 5:

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

Sper că ți-a plăcut lectura!