ES3, Chap 9. — La stratégie d'évaluation en JavaScript

Ce billet fait partie de la collection ES3 dans le détail et en constitue le Chapitre 9.

La magie du processus produit t-elle des copies d'objets ou de multiples références au même objet initial ?
La magie du processus produit t-elle des copies d'objets ou de multiples références au même objet initial ?

Dans cet article, nous allons étudier la stratégie de passage de paramètres (aussi appelé la stratégie de passage des arguments) via des fonctions JavaScript.

Introduction

On appel habituellement cette partie des sciences informatiques la stratégie d'évaluation, c.-à-d. un ensemble de règles et diverses expressions de calcul pour un langage de programmation. La stratégie de passage de paramètres est un cas spécial.

Beaucoup de développeurs sont certain que les objets en JavaScript (comme c'est le cas dans d'autres langages usuels) sont passés dans les fonctions par référence alors que les valeurs des types primitifs sont passées par valeur. De plus, nous pouvons lire ce « fait » dans divers articles, discussions sur les forums et même dans des livres sur le JavaScript. Mais, à quel point ce terme est t-il exact et à quel point cette description est exact ? Nous allons voir cela dans cet article.

Théorie générale

Revenons rapidement sur la théorie générale. Il y a deux sortes de stratégie d'évaluation de passage de paramètres : stricte, qui signifie que les arguments sont calculés avant d'être utilisés et non stricte, qui signifie dans ce cas, que les arguments sont calculés au moment où ils sont utilisés.

Nous considérons ici que des stratégies basiques de passage de paramètres à une fonction sont importante à comprendre du point de vue de JavaScript.

Et pour commencer cette explication, il est nécessaire de préciser qu'en JavaScript, tout autant que dans plusieurs autres langages (par exemple, C, Java, Python, Ruby, etc.) c'est la stratégie de passage de paramètres stricte qui est utilisée.

Aussi l'ordre dans lequel les arguments sont évalués est important. En JavaScript c'est de gauche à droite (contrairement à d'autres langages où c'est possiblement de droite à gauche).

La stratégie de passage stricte est également sous-divisée en plusieurs stratégies, la plus importante d'entre elle étant à considérer en détail.

Et parce que toutes les stratégies discutées ci-dessous n'existent pas en JavaScript, nous allons utiliser du pseudo code similaire à la syntaxe Pascal.

Appel par valeur

Ce type de stratégie est bien connu de beaucoup de développeurs. La valeur de l'argument ici est une copie de la valeur de l'objet passé par l'appelant en paramètre. Les changements fait aux arguments à l'intérieur d'une fonction n’influence pas l'objet passé depuis l'extérieur. Habituellement, il y a une nouvelle allocation de mémoire et la valeur de cet objet externe est copié puis une valeur identique depuis ce nouvel espace mémoire est utilisé dans la fonction.

Pseudo-code

alfred = 10

procedure theTransportedMan(gerald):
  gerald = 20;
end

theTransportedMan(alfred)

// les changements dans `theTransportedMan` n'ont pas d'effets
// depuis l'extérieur
print(alfred) // `10`

Cette stratégie pause de gros problèmes de performance dans le cas où le paramètre passé à la fonction n'est pas une valeur primitive, mais une structure complexe ou un objet. C'est ce qui se passe, par exemple, en C/C++ quand une structure est passée par valeur à une fonction. Elle est complètement copiée.

Décrivons l'exemple général que nous utiliserons pour la description des stratégies d'évaluation suivantes. Cette procédure abstraite accepte deux arguments : la valeur d'un objet ainsi qu'un booléen, qu'il soit nécessaire de modifier complètement la valeur de l'objet (ré-affectation de valeur), ou juste qu'il soit nécessaire de changer seulement les propriétés de l'objet (muter l'objet).

Pseudo-code

procedure theTransportedMan(gerald, isFullChange):

  if isFullChange:
    gerald = { z: 1, q: 2 }
    exit
  end

  gerald.x = 100
  gerald.y = 200

end

Pseudo-code

// définition de `theTransportedMan` ci-dessus

alfred = {
  x: 10,
  y: 20
}

theTransportedMan(alfred)

// avec un appel de stratégie par valeur,
// l'objet extérieur n'a pas changé.
print(alfred) // `{ x: 10, y: 20 }`

// c'est la même chose pour le changement complet.
// (affectation d'une nouvelle valeur)
theTransportedMan(alfred, true)

// aussi, aucun changements n'ont été fait
print(alfred) // `{ x: 10, y: 20 }`, et non pas `{ z: 1, q: 2 }`

Appel par référence

C'est au tour de la stratégie d'évaluation par référence (qui est également bien connue) qui ne reçoit pas une copie de la valeur, mais une référence à l'objet, c.-à-d. l'adresse mémoire directement en relation avec l'objet depuis l'extérieur. Tous les changements des arguments à l'intérieur de la fonction (affectation ou mutation) affecte l'objet à l'extérieur car l'adresse exacte de cet objet est lié à un paramètre formel, c.-à-d. un argument qui fait office d'alias pour l'objet depuis l'extérieur.

Pseudo-code

// même définition de `theTransportedMan` que précédemment

// avec le même objet
alfred = {
  x: 10,
  y: 20
}

// les résultats de la procédure `theTransportedMan`
// avec un appel par référence
// sont les suivants :

theTransportedMan(alfred)

// les valeurs de propriétés de l'objet ont changées
print(alfred) // `{ x: 100, y: 200 }`

// l'assignation d'une nouvelle valeur affecte
// également l'objet
theTransportedMan(alfred, true)

// `alfred` fait maintenant référence au nouvel objet
print(alfred) // `{ z: 1, q: 2 }`

Cette stratégie permet de passer plus efficacement des objets complexes, par ex. des grosses structures avec une quantité considérable de propriétés.

Appel par partage

Alors que les deux premières stratégies d'évaluation sont connues de la majorité des développeurs, cette stratégie (et pour être plus exact, ce terme) n'est pas très répandue. Mais comme nous allons le voir rapidement, elle joue un rôle essentielle dans le passage des paramètres en JavaScript.

Des noms alternatifs pour cette stratégie sont « appel par objet » ou « appel par partage d'objet ».

La stratégie « par partage » a été nommée en premier par Barbara Liskov pour le langage de programmation CLU en 1974.

Le point principal de cette stratégie est que la fonction reçoit une copie de la référence de l'objet. Cette copie de référence est associée à un paramètre formel et est sa valeur.

Même si le concept de référence dans ce cas semble apparaître, cette stratégie ne devrait pas être traitée comme un appel par référence (et donc dans ce cas, la majorité font l'erreur), car la valeur de l'argument n'est pas directement l'alias, mais une copie de l'adresse mémoire.

La différence principale vient du fait que l'affectation d'une nouvelle valeur à l'argument à l'intérieur de la fonction n'affecte pas l'objet à l'extérieur (ce qui aurait été le cas d'un appel par référence). Cependant, parce que c'est un paramètre formel, qu'il a une copie de l'adresse mémoire ; il a accès au même objet qu'à l'extérieur (c.-à-d. que l'objet à l'extérieur n'est pas copié comme ça aurait été le cas d'un appel par valeur) et les changements des propriétés de l'argument local (les mutations) affecte l'objet à l'extérieur.

Pseudo-code

// même définition de `theTransportedMan` que précédemment

// encore avec le même objet
alfred = {
  x: 10,
  y: 20
}

// l'appel par partage
// affecte l'objet
// de la manière suivante

theTransportedMan(alfred)

// les valeurs de propriétés d'objet ont changées
print(alfred) // `{ x: 100, y: 200 }`

// mais avec un changement d'objet
// il n'y a pas de changement
theTransportedMan(alfred, true)

// c'est donc la même chose que l'appel précédent.
print(alfred) // `{ x: 100, y: 200 }`

Cette stratégie prend en compte le fait que le langage opère en majorité avec des objets, au lieu de valeurs primitives.

Par partage : un cas spécial de par valeur

La stratégie par partage est utilisé dans divers langages : Java, JavaScript, Python, Ruby, Visual Basic, etc.

Cependant, c'est dans la communauté Python que le terme par partage est utilisé. Car dans les autres langages, il y a des termes alternatifs utilisés qui peuvent préter à confusion car il sont le contraire de stratégies avec le même nom dans d'autres langages.

Dans la plupart des cas, par exemple, en Java, JavaScript ou Visual Basic, cette stratégie est aussi nommée par valeur, signifiant, valeur spécifique / copie de référence.

D'un côté, c'est vrai, affecter à un argument dans une fonction va seulement lier son nom avec une nouvelle valeur (nouvelle adresse mémoire) et ne va pas influancer l'objet extérieur.

D'un autre côté, ce terme n'est pas totalement correct si on examine la question en profondeur.

La théorie générale a un cas spéciale d'appel par valeur avec comme valeur spécifique, la copie par adresse. Par conséquent, ces technologies ne rompent pas les règles terminologiques.

En Ruby, cette stratégie est nommée par référence. Encore, d'un côté on ne passe pas réellement la copie d'une grosse structure (c.-à-d., pas par valeur), mais d'un autre côté, nous ne jouons pas avec la référence originale de l'objet, et on ne peut donc pas le changer. Encore une fois, ce mélange de terme prête à confusion.

La théorie générale ne décrit aucun cas spéciale d'appel par référence comme c'est le cas pour un appel par valeur.

Il est nécessaire de bien comprendre que toutes ces appellations mentionnés dans divers langages (Java, JavaScript, Python, Ruby, etc.) ont un nom revisité. Dans la théorie générale ils font référence en réalité à l'appel par partage.

Par partage et pointeurs

Par rapport au C / C++, cette stratégie de passage est idéologiquement similaire au passage de valeur par pointeur, mais avec une différence importante ; il est possible de déréférencer le pointeur et ne pas changer complètement l'objet. Mais en général, l'affectation d'une valeur (adresse) au pointeur la lie avec le nouveau bloc de mémoire (c.-à-d. que le bloc de mémoire auquel le pointeur était référencé avant reste intact) ; et les changements de propriétés des objets référencés via le pointeur influent sur l'objet externe.

Par conséquent, en faisant une analogie avec les pointeurs, nous pouvons voir en passant par valeur de l'adresse, ce qu'est exactement un pointeur. Dans ce cas, par partage est une sorte de « sucre syntaxique » qui a l'affectation se comporte comme un pointeur (mais qu'il est impossible de déférencer), et dans le cas de changement de propriété, comme une référence (qui ne requière pas d'opération de déréférencement). Parfois, cela peut être nommé un « pointeur sécurisé ».

Cependant, C / C++ ont également un « sucre » pour référencer des propriétés d'objets sans déréférencer les pointeurs :

Code C / C++

obj->x
// au lieu de
(*obj).x

Le comportement le plus proche du passage par partage en C++ peut être associé aux implémentations de « pointeurs intelligent », par exemple avec boost::shared_ptrqui surcharge l'opérateur d'affectation et copie le constructeur à ces fins et ainsi utilise un comptage de référence d'objets, supprimant les objets par GC. Ce type de donnée à même un nom similaire sharedptr (« pointeurpartagé »).

Implémentation JavaScript

Maintenant nous connaissons la stratégie d'évaluation pour le passage de paramètres qui est utilisée en JavaScript. L'appel par partage : la mutation des propriétés de l'argument influence l'objet externe mais l'affectation d'une nouvelle valeur à l'argument n'influence pas l'objet externe.

Un terme lourd utilisable pour la stratégie de passage utilisée pourrait donc être « appel par valeur où la valeur est une copie de référence ».

L'inventeur du JavaScript, Brendan Eich a également metionné que c'est une copie de référence (copie par adresse) qui est passée.

Plus précisément, ce comportement peut être compris et considéré comme une simple affectation où il y a deux objets différents, mais avec une valeur identique (la copie d'adresse).

Code JavaScript

var robertAngier = { x: 10, y: 20 };
var theRealTransportedMan = robertAngier;

alert(theRealTransportedMan === robertAngier); // `true`

theRealTransportedMan.x = 100;
theRealTransportedMan.y = 200;

alert([robertAngier.x, robertAngier.y]); // `[100, 200]`

C.-à-d. que deux identifiants (liaison de nom) sont liés au même objet en mémoire, partageant cet objet :

Pseudo-code

robertAngierValue: addr(0xFF) /* ----> `{ x: 100, y: 200 }` address 0xFF <----  */ theRealTransportedManValue: addr(0xFF)

Et l'affectation lie seulement un identifiant à un nouvel objet (avec la nouvelle adresse) mais n’influence pas l'objet précédemment lié_ comme cela aurait été le cas avec une référence :

Code JavaScript

theRealTransportedMan = { z: 1, q: 2 };

alert([robertAngier.x, robertAngier.y]); // `[100, 200]` Rien n'a changé
alert([theRealTransportedMan.z, theRealTransportedMan.q]); // `[1, 2]` cependant `theRealTransportedMan` fait maintenant référence au nouvel objet.

C.-à-d. que maintenant robertAngier et theRealTransportedMan ont une valeur différente, à des adresses différentes

Pseudo-code

robertAngierValue: addr(0xFF) /* ----> `{ x: 100, y: 200 }`, address 0xFF */
theRealTransportedManValue: addr(0xFA) /* ----> `{ z: 1, q: 2 }`, address 0xFA */

Encore une fois, tout est lié au fait que les valeurs de variable dans le cas d'un type objet sont adressée, mais ne sont pas la structure de l'objet en elle-même. L'affectation d'une variable dans une autre copie sa valeur de référence, et donc les deux variables référence le même emplacement mémoire. La prochaine affectation d'une nouvelle valeur (la nouvelle adresse) va délier le nom de l'ancienne adresse et le lié à la nouvelle. C'est la principale différence avec une stratégie par référence.

Si vous ne considérez que le niveau d'abstraction fournit par de standard ECMA-262, vous ne verrez que le concept de « valeur » dans tous les algorithmes. L'implémention du passage de cette « valeur » (et de ses variantes ; primitives ou objet) n'est pas mise en avant. De ce point de vu, en s’appuyant sur l'abstraction ECMASCript, il est possible de dire précisément et exactement qu'il n'y a que la « valeur » et, en s'accordant au nommage utilisé, seulement des appels par valeurs.

Pour éviter des malentendus (pourquoi les propriétés d'un objet externe peuvent être changés depuis une fonction), il est nécessaire de considérer en détail le niveau d'implémentation qui est l'appel par partage, ou plus lourdement « par pointeur sécurisé qu'il est impossible de déréférencer et d'en changer complètement l'objet, mais dont il est possible de changer les propriétés ».

Versions de terme

Ce peut être « appel par valeur », en spécifiant que c'est le cas spéciale par valeur qui signifie quand la valeur est une copie d'adresse mémoire. De ce point de vu il est possible de dire que tous les objets sans exception en JavaScript sont passés par valeur, c'est ce qui est actuellement expliqué dans l'abstraction ECMAScript.

Ce peut aussi être « appel par partage », qui permet de mettre en évidence la différence avec l'appel classique par valeur ou l'appel par référence. Dans ce cas il est possible de diviser les types passés : les valeurs primitives sont passées par valeur et les objets par partage.

L'affirmation « les objets sont passés au fonction par référence » formulé ainsi n'est pas vrai en JavaScript et est incorrecte.

Conclusion

J'espère que cet article vous aura aider à comprendre plus en détail l'évaluation de stratégie dans son ensemble et plus particulièrement dans le cas du JavaScript. Ceci met fin à notre petit parcours ES3. Je vous dis à bientôt pour quelques articles ES5 !

Références

Lectures additionnelles :

Ce texte est une libre adaptation française d'une partie de l'excellent billet Тонкости ECMA-262-3. Часть 8. Стратегия передачи параметров в функцию. de Dmitry Soshnikov.