Héritage et classes en JavaScript

Ce billet date de plusieurs années, ses informations peuvent être devenues obsolètes.

Voir aussi Héritage et classes en JavaScript (bis).

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 et plus simple à concevoir.

On va donc parler héritage par prototype. 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, c'est à dire une fonction destinée à être utilisée avec new dont le nom, par convention, commence par une majuscule

Au moment de la création d'un objet, un [[Prototype]] lui est associé.

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
  • Function.prototype est associé aux objets de type fonction
let foo = {x: 10}
foo.__proto__ === Object.prototype

let bar = function () {}
bar.__proto__ === Function.prototype
// Function.prototype is linked to Object.prototype
bar.__proto__.__proto__ === Object.prototype

Quand 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).

Quand la fonction est utilisée en tant que constructeur (via new donc) pour créer une nouvelle instance, l'objet référencé par la propriété prototype devient le [[Prototype]] de la nouvelle instance :

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 instances. 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 ils sont un peu tombés en désuétude.

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 :

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 :

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

car.__proto__.__proto__ === Vehicle.prototype

Ressources

Avant Fibonacci, tu peux pas test Après Prélèvement à la source pour les TNS à l'IS

Tag Kemar Joint