Ouvrir la navigation secondaire
Précédent Fibonacci, tu peux pas test
Suivant Prélèvement à la source pour les TNS

Héritage et classes en JavaScript

L'héritage en JavaScript est un sujet très allambiqué à cause de la confusion introduite par certains termes et par certains noms d'opérateurs (comme new ou instanceof) qui laissent penser que le langage dispose d'un système d'héritage classique.

Du coup les gens ont toujours cherché à reproduire un tel système d'héritage en JavaScript.

À tel point qu'ES6 à décidé de masquer l'héritage prototypal du langage derrière une syntaxe plus classique. Ce que ça veut dire c'est que l'héritage classique gagne toujours à la fin parce qu'il est plus simple à concevoir pour le cerveau.

On va quand même parler héritage prototypal. Et pour bien comprendre comment ça fonctionne, il faut d'abord clarifier certaines notions qui prêtent à confusion. Une fois que ça sera bien clair, vous pourrez aller lire n'importe quel article sur le sujet car il y en a une flopée !

[[Prototype]] et prototype, constructeur et constructor

En JavaScript, un objet peut être créé (entre autres manières) via :

  • la notation littérale
  • un constructeur (un constructeur est simplement une fonction destinée à être utilisée avec new et dont le nom, par convention, commence par une majuscule)

Au moment de la création d'un objet et sauf indication contraire, un [[Prototype]] lui est associé (implicitement ou explicitement). Un [[Prototype]] est simplement un autre objet pouvant être partagé avec d'autres objets (la base du mécanisme d'héritage du langage).

Dans le cas d'une création d'objet via notation littérale, Object.prototype est associé aux objets classiques et Function.prototype est associé aux objets de type fonction :

let foo = {x: 10}
Object.getPrototypeOf(foo)
foo.__proto__ === Object.prototype

let bar = function () {}
Object.getPrototypeOf(bar)
bar.__proto__ === Function.prototype
bar.__proto__.__proto__ === Object.prototype

Si l'objet créé est de type fonction, il reçoit en sus et de manière automatique une propriété prototype, à distinguer de [[Prototype]] ! La valeur de prototype est un objet contenant une seule propriété constructor pointant vers la fonction elle-même. Le nom de la propriété constructor induit en erreur car cette propriété n'est pas utilisée lors de la création des objets et ne sert pas à grand chose tant que vous ne l'utilisez pas explicitement dans votre code. Certains vont même jusqu'à préconiser d'éviter son usage.

Dans le cas d'une création d'objet via un constructeur (avec new donc), l'objet référencé par la propriété prototype devient le [[Prototype]] du nouvel objet :

let Car = function (name) {
  this.name = name,
  this.wheels = 4
}
Object.getOwnPropertyNames(Car.prototype)
typeof Car.prototype.constructor
Car.prototype.constructor === Car

let car1 = new Car('alpine')
car1.__proto__ === Car.prototype
car1 instanceof Car

let car2 = new Car('panda')
Object.getOwnPropertyNames(car2)
car2.__proto__ === Car.prototype
car2.__proto__ === car1.__proto__

Car.prototype.hootTheHorn = function () {
    console.log(`Beep, beep, this is ${this.name}!`)
}
car1.hootTheHorn()
car2.hootTheHorn()

Pour contrôler l'héritage, le jeu est de manipuler le [[Prototype]] afin d'y stocker des méthodes et des propriétés pour les partager entre des objets. Pour ce faire il est possible d'utiliser les constructeurs et le mot-clé new, Object.create (introduit dans ECMAScript 5) ou la syntaxe native d'ES6.

Anciens patterns d'héritage classique

Vous verrez peut-être encore ces patterns dans d'anciens projets. Ils sont décrits dans le chapitre Code Reuse Patterns de JavaScript Patterns.

Je ne vais pas m'étaler sur ces patterns car d'une part ils sont un peu tombés en désuétude, et d'autre part c'est franchement pénible et chiant à lire.

Héritage avec ECMAScript 5

Avec l'arrivée d'Object.create, la syntaxe pour simuler l'héritage devient plus explicite mais reste quand même bien verbeuse :

function Vehicle (name) {
  this.name = name
}

Vehicle.prototype.describe = function () {
  return `Car named ${this.name}`
}

function Car (name, color) {
    // Invoke the parent's constructor: super(name).
    Vehicle.call(this, name)
    // Specific properties.
    this.color = color
}

// Inherit from parent's [[Prototype]].
Car.prototype = Object.create(Vehicle.prototype)

// If needed, Use Car as own constructor
// instead of the inherited one.
Car.prototype.constructor = Car

Car.prototype.describe = function () {
  // Invoke parent's describe method: super.describe().
  let superDescribe = Vehicle.prototype.describe.call(this)
  return `${superDescribe} (${this.color})`
}

let car = new Car('Alpine', 'Blue')
car.describe()

Inspection des travaux finis :

Object.getPrototypeOf(car)
car.__proto__ === Car.prototype
Object.getOwnPropertyNames(car.__proto__)

car.__proto__.__proto__ === Vehicle.prototype

Héritage avec ECMAScript 2015

Il aura fallu attendre ES6 pour pouvoir utiliser une syntaxe plus compacte et simple à lire qui ressemble à un système d'héritage classique :

class Vehicle {
  constructor (name) {
    this.name = name
  }
  describe () {
    return `Car named ${this.name}`
  }
}

class Car extends Vehicle {
  constructor (name, color) {
    super(name)
    this.color = color
  }
  describe () {
    return `${super.describe()} (${this.color})`
  }
}

let car = new Car('Alpine', 'Blue')
car.describe()

Inspection des travaux finis :

Object.getPrototypeOf(car)
car.__proto__ === Car.prototype
Object.getOwnPropertyNames(car.__proto__)

car.__proto__.__proto__ === Vehicle.prototype

Ressources