Herència i la cadena de prototipus

This article needs an editorial review. How you can help.

JavaScript pot resultar una mica confús per als desenvolupadors amb experiència en llenguatges bassats en classes (com ara Java o C++), ja que degut al seu dinamisme no proporciona una implementació de class per se (la paraula clau class va ser introduïda al ES6, però sintàcticament és irrellevant, JavaScript continuarà basat en prototipus).

Quant a l'herència, JavaScript només disposa d'una construcció: objectes. Tots els objecte tenen un enllaç a un altre objecte, anomenat el seu prototipus. Aquest objecte prototipus també té el seu propi prototipus, i així es crea una cadena fins que s'arriba a un objecte que tingui null com a prototipus. null, per definició, no te prototipus, i actua com a enllaç final en aquesta cadena de prototipus.

A pesar de que això es consideri freqüentment com a un dels punts dèbils de JavaScript, el model d'herència basat en prototipus és, de fet, més potent que el model clàssic. És, per exemple, força senzill construir un model clàssic mitjançant un model basat en prototipus, mentre que fer-ho al revès és una qüestió força més complexa.

Herència mitjançant la cadena de prototipus

Propietats heretades

Els objectes a JavaScript són "bosses" dinàmiques de propietats (referenciades com a propietats pròpies). Els objectes a JavaScript tenen una referènia a un objecte prototipus. A l'hora d'intentar accedir a una propietat d'un objecte, no només es cercarà la propietat dins l'objecte mateix, sinò també al prototipus de l'objecte, el prototipus del prototipus, i així fins a que o bé es trobi la propietat amb el nom que es buscava o bé es troba el final de la cadena de prototipus.

Tot seguint l'standard ECMAScript, utilitzem la notació unObjecte.[[Prototipus]] per a designar el prototipus de unObjecte. El resultat és el mateix que el de la propietat __proto__ (ara ja en desús). A partir de l'ECMAScript 6, s'accedeix al [[Prototipus]] mitjançant els accessors Object.getPrototypeOf() i Object.setPrototypeOf().

A continuació es mostra el que succeeix quan s'intenta accedir a una propietat:

// Assumim que disposem de l'objecte o, amb les seves pròpies propietats a i b:
// {a: 1, b: 2}
// o.[[Prototipus]] té les propietats b i c:
// {b: 3, c: 4}
// Finalment, o.[[Prototipus]].[[Prototipus]] és null.
// Aquest és el final de la cadena de prototipus ja que null,
// per definició, no té [[Prototipus]].
// Llavos, la cadena de prototipus sencera és la següent:
// {a:1, b:2} ---> {b:3, c:4} ---> null

console.log(o.a); // 1
// Hi ha una propietat 'a' pròpia a l'objecte o? Si, i el seu valor és 1.

console.log(o.b); // 2
// Hi ha una propietat 'b' pròpia a l'objecte o? Si, i el seu valor és 2.
// El prototipus també té una propietat 'b', però aquest no s'arriba a cercar. 
// Aquest fenòmen es coneix com a "property shadowing"

console.log(o.c); // 4
// Hi ha una propietat 'c' pròpia a l'objecte o? No, cerca-la al seu prototipus.
// Hi ha una propietat 'c' pròpia a o[[Prototipus]]? Si, i el seu valor és 4.

console.log(o.d); // undefined
// Hi ha una propietat 'd' pròpia a l'objecte o? No, cerca-la al seu prototipus.
// Hi ha una propietat 'd' pròpia a o[[Prototipus]]? No, cerca-la al seu prototipus.
// o.[[Prototipus]].[[Prototipus]] és null, atura la cerca,
// no s'ha trobat la propietat, retornem undefined

Assignar una propietat a un objecte crea una propietat pròpia. L'única excepció al les regles de comportament d'assignació i obtenció és quan hi ha una propietat heretada que disposa d'un getter o un setter.

Herència de "mètodes"

JavaScript no té "mètodes" de la manera en que els llenguatges bassats en classes els defineixen. A JavaScript, qualsevol funció pot ser afegida a un objecte com a propietat. Una funció heretada actua com qualsevol altra propietat, incloent shadowing de propietats, tal i com es mostra més adalt (en aquest cas, una forma de sobreescritura de mètodes).

Quan s'executa una funció heretada, el valor de this passa a apuntar a l'objecte que l'hereta, no a l'objecte prototipus al qual la funció pertany com a propietat pròpia.

var o = {
  a: 2,
  m: function(b){
    return this.a + 1;
  }
};

console.log(o.m()); // 3
// Al cridar a o.m en aquest cas, 'this' fa referència a o

var p = Object.create(o);
// p és un objecte que hereta de o

p.a = 12; // crea una propietat pròpia 'a' a l'objecte p
console.log(p.m()); // 13
// quan es crida p.m 'this' fa referència a p.
// Així quan p hereta la funció m de o, 
// 'this.a' s'avalua com a p.a, la propietat pròpia 'a' de p

Formes diferents de crear objectes i la cadena de prototipus resultant

Crear objectes amb sintaxi de construcció

var o = {a: 1};

// L'objecte recentment creat o té Object.prototype com al seu [[Prototipus]]
// o no te cap propietat pròpia anomenada 'hasOwnProperty'
// hasOwnProperty és una propietat pròpia de Object.prototype. 
// Així o hereta hasOwnProperty de Object.prototype
// El prototipus de Object.prototype val null.
// o ---> Object.prototype ---> null

var a = ["yo", "whadup", "?"];

// Els Arrays hereten de Array.prototype 
// (el qual té mètodes com ara indexOf, forEach, etc.)
// La cadena de prototipus té la forma següent:
// a ---> Array.prototype ---> Object.prototype ---> null

function f(){
  return 2;
}

// Les funcions hereten de Function.prototype 
// (que té mètodes com ara call, bind, etc.)
// f ---> Function.prototype ---> Object.prototype ---> null

Crear objectes mitjançant un constructor

A JavaScript, un "constructor" és "simplement" una funció que es crida amb l'operador new.

function Graph() {
  this.vertices = [];
  this.edges = [];
}

Graph.prototype = {
  addVertex: function(v){
    this.vertices.push(v);
  }
};

var g = new Graph();
// g és un objecte amb propietats pròpies 'vertices' i 'edges'.
// g.[[Prototipus]] és el valor de Graph.prototype quan s'executa new Graph().

Crear objectes mitjançant Object.create

ECMAScript 5 va introduïr un nou mètode: Object.create(). Cridar a aquest mètode crea un nou objecte. El prototipus d'aquest objecte es el primer argument de la funció:

var a = {a: 1}; 
// a ---> Object.prototype ---> null

var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (heretat)

var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null

var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); 
// undefined, perque d no hereta de Object.prototype

Crear objectes mitjançant la paraula clau class

ECMAScript 6 va introduïr un nou conjunt de paraules clau per a implementar classes. A pesar de que aquestes construccions s'assemblen a les que es poden trobar a llenguatges bassats en classes, no són iguals. JavaScript continua sent un llenguatge bassat en prototipus. Les noves paraules clau inclouen class, constructor, static, extends, i super.

"use strict";

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

class Square extends Polygon {
  constructor(sideLength) {
    super(sideLength, sideLength);
  }
  get area() {
    return this.height * this.width;
  }
  set sideLength(newLength) {
    this.height = newLength;
    this.width = newLength;
  }
}

var square = new Square(2);

Rendiment

El temps que es triga a resoldre propietats que estan lluny dins la cadena de prototipups pot tenir un impacte negatiu en el rendiment, i aquest impacte pot ser significatiu en parts del codi on el rendiment és crític. A més, intentar accedir a propietats que no existeixen sempre atravessarà totalment la cadena de prototipus.

Adicionalment, quan s'itera sobre propietats d'un objecte, totes les propietats enumerables de la cadena de prototipus seran enumerades.

Per a comprovar si un objecte té una propietat definida en si mateix (pròpia) en comptes de heretada de la cadena de prototipus, s'ha de fer anar el mètode hasOwnProperty, el qual és heretat per tots els objectes mitjançant Object.prototype.

Nota: No n'hi ha prou amb comprovar si una propietat és undefined. La propietat pot existir perfectament, però tenir assignat el valor undefined.

 

Males pràctiques: Extensió de prototipus nadius

Una característica que sovint s'utilitza però no s'hauria de fer anar és extendre Object.prototype o qualsevol altre prototipus nadiu del sistema.

Aquesta tècnica, anomenada monkey patching, trenca l'encapsulació. A pesar de ser utilitzada per llibreries populars, com ara Prototype.js, no hi ha cap bona raó per a abarrotar els tipus nadius amb funcionalitat no standard.

La única possible bona raó per a extendre un prototipus nadiu és per a afegir característiques dels nous motors de JavaScript no suportades pel motor actual (funcions polifill), com ara Array.forEach, etcètera.

Exemple

B hauria d'heretar de A:

function A(a){
  this.varA = a;
}

// Quin és el propòsit d'incloure varA al prototipus quan A.prototype.varA sempre estarà amagat (shadowed) per
// this.varA, donada la definició de la funció A de dalt?
A.prototype = {
  varA : null,  // No hauriem de treure varA del prototipus ja que no fa res?
      // potser es tracta d'una optimització per a alleujar espai a classes ocultes?
      // https://developers.google.com/speed/articles/optimizing-javascript#Initializing instance variables
      // seria vàlid si varA no s'inicialitzés de forma única per a cada instància
  doSomething : function(){
    // ...
  }
}

function B(a, b){
  A.call(this, a);
  this.varB = b;
}
B.prototype = Object.create(A.prototype, {
  varB : {
    value: null, 
    enumerable: true, 
    configurable: true, 
    writable: true 
  },
  doSomething : { 
    value: function(){ // sobreescriu
      A.prototype.doSomething.apply(this, arguments); // crida a super
      // ...
    },
    enumerable: true,
    configurable: true, 
    writable: true
  }
});
B.prototype.constructor = B;

var b = new B();
b.doSomething();

Les parts importants són:

  • Els tipus es defineixen a .prototype
  • S'utilitza Object.create() per a heretar

prototype i Object.getPrototypeOf

JavaScript pot resultar una mica confús per a desenvolupadors que vinguin de Java o C++ degut a que és completament dinàmic, tot succeeix en temps d'execució i no té classes. Només té instàncies (objectes). Fins i tot les "classes" que simulem són simplement un objecte de tipus funció.

Potser ja us heu adonat que la nostra funció A té una propietat especial anomenada prototype. Aquesta propietat especial funciona en conjunció amb l'operador de JavaScript new. La referència a l'objecte prototipus es copia a la propietat interna [[Prototype]] de la nova instància. Per exemple, amb el codi var a1 = new A(), JavaScript (després de crear l'objecte en memòria però abans d'executar la funció A() amb this definit i apuntant a si mateixa) assigna a1.[[Prototype]] = A.prototype. Després, quan accedim a les propietats de la instància, JavaScript primer comprova si aquestes existeixen dins l'objecte mateix i, en cas contrari, les cerca dins el [[Prototipus]]. Això implica que tot allò que és definit al prototype és compartit per totes les instàncies, i més tard es poden canviar parts de prototype de forma que els canvis afectaran a totes les instàncies que existeixen, si es vol.

Si, a l'exemple anterior, s'executa var a1 = new A(); var a2 = new A(); llavors a1.doSomething faria referència en realitat a Object.getPrototypeOf(a1).doSomething, que és el mateix que A.prototype.doSomething que heu definit, per exemple, Object.getPrototypeOf(a1).doSomething == Object.getPrototypeOf(a2).doSomething == A.prototype.doSomething.

Resumint, prototype és per als tipus mentre que Object.getPrototypeOf() és el mateix per les instàncies.

[[Prototype]] s'accedeix de forma recursiva, per exemple: a1.doSomething, Object.getPrototypeOf(a1).doSomething, Object.getPrototypeOf(Object.getPrototypeOf(a1)).doSomething etc., fins que es troba o bé Object.getPrototypeOf retorna null.

Així, quan cridem:

var o = new Foo();

JavaScript simplement executa:

var o = new Object();
o.[[Prototype]] = Foo.prototype;
Foo.call(o);

(o quelcom similar) i quan després executem:

o.someProp;

comprova si o té definida la propietat someProp. En cas que no, comprova Object.getPrototypeOf(o).someProp i si aquesta tampoc existeix llavors comprova Object.getPrototypeOf(Object.getPrototypeOf(o)).someProp, etcètera.

Conclusions

Resulta esencial entendre el model d'herència basat en prototipus abans de començar a escriure codi complex que es basi en ell. Així mateix, és important tenir en compte la longitud de la cadena de prototipus dins el nostre codi i trencar-la en cas necesari, per a evitar possibles problemes de rendiment. A més els prototipus natius no s'haurien d'extendre mai a no ser que el motiu sigui afegir compatibilitat amb característiques més modernes de JavaScript.

Document Tags and Contributors

 Contributors to this page: enTropy
 Last updated by: enTropy,