Comprendre les fonctions et le mot clé This
Le mot clé This
Javascript a beaucoup évolue depuis ses premières version, et beaucoup de concept de base ont vu leur fonctionnement modifié.
Il est donc nécessaire de faire un tour d’horizon et revoir ensemble les fondamentaux de Javascript.
Dans un premier temps, prenons une fonction simple
1function myFunction() {
2 console.log(this===window);
3}
4myFunction() // affiche : "true"
Cette fonction ne fait pas grand chose ormi comparer la stricte égalité de this
avec window
.
window
est une variable globale qui represente la fenêtre du navigateur. Si l’on écrirait ce code depuis NodeJs, nous aurions mis à la place global
.
En executant ce bout de code, nous voyons que la valeur de retour est true
.
Compliquons légérement les choses, créons un objet qui contiendra une fonction qui fera la meme chose que précédement.
1var myObject = {
2 myFunction: function() {
3 console.log(this === myObject);
4 }
5}
6
7myObject.myFunction(); // affiche : "true"
L’utilisation de cet exemple permet de montrer que le pointeur this
est placé sur l’objet myObject
et non plus sur window
.
Et cela fonctionne comme nous pourrions l’attendre, this
sert à appeler le contexte dans lequel une fonction est associée.
Et c’est le comportement que l’on retrouve dans des langages orientés objets comme le C# ou java.
Avertissement
Le terme pointeur employé ici n’a rien à voir avec le concept de pointeur en C.
Changeons maintenant légérement les choses dans notre code. Nous allons garder exactement la même déclaration de l’objet myObject
, mais lieu d’appeler myFunction
depuis l’objet directement nous allons créer une variable myFunction
qui contiendra myObject.myFunction
.
1var myObject = {
2 myFunction: function() {
3 console.log(this === myObject);
4 }
5}
6
7var myFunction = myObject.myFunction;
8myFunction(); // affiche : "false"
Nous voyons que maintenant this
n’est plus égale au contexte de la fonction myFunction
mais à window
.
Régle Javascript
Si il est appelé depuis obj.func()
alors this
égale obj
. C’est à dire depuis la fonction elle-même dans son contexte.
Sinon , this
égale window
ou global
.
C’est comme cela que les choses fonctionnent avec Javascript.
Prenons un autre exemple :
1var myObject = {
2 myFunction: function() {
3 console.log(this===myObject); // affiche: "true"
4 setTimeout(function() {
5 console.log(this === myObject); // affiche: "false"
6 console.log(this === window); // affiche: "true"
7 },0);
8 }
9}
10myObject.myFunction();
Nous retrouvons myObject
mais nous y avons ajouté une fonction asynchrone setTimeout
(Nous aurions pu utiliser n’importe quelle autre fonction qui possède un callback
).
Et nous appelons la fonction myFunction
depuis l’objet directement.
nous constatons qu’à la ligne 3, this
est « égale » à myObject
.
Alors qu’à la ligne 5, dans la fonction callback, this
égale à window
à la place de myObject
.
Pourquoi cela ? Pour comprendre, il faut se rapporter à la règle émise plus haut.
A la ligne 3, this
est invoqué depuis myFunction
par l’intermédiaire de la référence à l’objet myObject
elle-même.
Or, à la ligne 5, this
est appelé depuis une fonction anonyme, qui n’est référencé dans aucun objet. C’est la fonction setTimeout
qui l’appelle.
Donc this
égal à window
.
Expression de fonction vs Déclaration de fonction
1function myFunctionDeclaration() {}
2
3var myFunctionExpression = function() {};
Depuis ES5, il possible de déclarer des fonctions Javascript de 2 manières différentes comme le montre le code ci-dessus.
Ces déclarations semblent différentes mais font exactement la même chose.
Toutefois comme nous l’avons expliqué dans le cours sur le hoisting
il existe une différence lors de la déclaration et de l’invocation de la fonction.
Nous pouvons parfaitement utiliser une fonction déclarée après son appel, car le hoisting
va se charger de remonter la déclaration tout en haut du script.
1myfunction(); // Affiche: "Hello"
2
3function myFunction() {
4 console.log("hello");
5}
Par contre nous aurons une erreur en utilisant la syntaxe suivante :
1myfunction(); // Affiche: Uncaught ReferenceError: myfunction is not defined"
2
3var myFunction = function () {
4 console.log("hello");
5}
Car le mécanisme de hoisting
sépare les variable en deux parties: La déclaration et l’affectation. Il déplace la partie déclarative en haut du script et laisse l’affectation là où elle a été mise.
Dans notre code var myFunction
est considéré comme une déclaration de variable et c’est ce qu’elle est : une variable auquelle est affectée une référence à une fonction anonyme. Et à la ligne 1, myFunction
égale à undefined
.
1var myFunction;
2
3myfunction();
4
5myFunction = function () {
6 console.log("hello");
7}
Expressions de fonction nommée
1var myFunction = function myOtherFunction(recurse) {
2 if(recurse) {
3 myFunction(false); // OK
4 myOtherFunction(false); // OK
5 }
6};
7
8myFunction(true); // OK
9myOtherFunction(true); // ReferenceError
Nous avons déclaré une fonction nommée myOtherFunction
dont la référence est assignée à la variable myFunction
.
A l’intérieur de myOtherFunction
, nous appelons : myFunction
et myOtherFunction
, et nous avons le droit de le faire.
Par contre, si à l’extérieur nous appelons myOtherFunction
directement, nous aurons un message d’erreur de référence. Seul l’appel par myFunction
sera valide.
Call
, apply
et bind
: initialisation manuelle de this
Précédement, nous avons mis en évidence que this
est de nouveau assigné à global
ou window
s’il est utilisé au sein d’une fonction asynchrone.
Etudions avec ce script comment changer la valeur de this
dans une fonction avec les méthodes call
, apply
et bind
.
call
Etudions le cas de la méthode call
:
1var myObject = {
2 myFunction: function(a, b) {
3 console.log(a + ' ' + b); // affiche : "Hello world"
4 console.log(this === myObject); // False
5 console.log(this === myOtherObject); // True
6 }
7}
8
9var myOtherObject = {}
10
11myObject.myFunction.call(myOtherObject, 'hello', 'world');
Nous créons un objet quelconque : myOtherObject
.
Nous appellons la méthode myFunction
de l’objet myObject
, mais nous souhaitons que la référence de this
de myFunction
soit celle d’un autre objet extérieur, myOtherObject
. Cela est possible grâce à la méthode call
, qui prend en premier argument l’objet dont vous voulons utiliser la référence et les autres arguments suivants seront ceux nécessaires à l’utilisation de la fonction myFunction
.
apply
Il existe une autre syntaxe qui fait exactement la même chose que call
mais avec la méthode apply
. La seule différence réside dans la manière dont sont passées les arguments à la fonction : ils sont placés dans un tableau.
myObject.myFunction.apply(myOtherObject, ['hello', 'world']);
bind
Et finalement nous avons bind
qui fonctionne presque pareil que call
ormi du fait qu’il sépare la procédure d’utilisation en deux étapes séparées.
1var myObject = {
2 myFunction: function(a, b) {
3 console.log(a + ' ' + b); // affiche : "Hello world"
4 console.log(this === myObject); // False
5 console.log(this === myOtherObject); // True
6 }
7}
8
9var myOtherObject = {}
10
11var myFunction = myObject.myFunction.bind(myOtherObject);
12myFunction('hello', 'world');
Nous obtenons une nouvelle fonction qui possède un contexte de this
prédéfinie, qui n’est pas celui de l’objet parent dans laquelle la fonction est déclarée, mais de l’objet myOtherObject
passé en argument à la méthode bind
. Nous l’avons assigné à une variable qui peut être ensuite utilisé comme une fonction classique.
bind
est typiquement utilisé si nous avons besoin de forcer le pointeur de this
d’une fonction callback par exemple.
Notation abrégée des objets
1const myObject = {
2 myFunction() {
3 console.log(this === myObject);
4 }
5};
6
7myObject.myFunction(); // true
8
9const myFunction = myObject.myFunction;
10
11myFunction(); // false
Nous utilisons ici const
pour déclarer notre objet myObject
à la place de var
.
Et contrairement aux exemples précédents nous avons déclaré la fonction myFunction
directement en la nommant sans utiliser le mot clé function
et sans l’avoir assigné à une clé d’objet comme : objectKey : function() {}
.
Cette nouvelle syntaxe introduite par ECMA2015 permet de raccourcir les déclarations de fonction dans un objet Javascript tout en restant lisible.
A la ligne 7, nous voyons que le pointeur this
de la fonction myFunction
est égale à son objet parent.
Toutefois lorsque nous faisons un alias à la ligne 9, this
change de valeur.
Cette syntaxe offre donc le même comportement pour la valeur de this
qu’avec une syntaxe avec les :
et le mot clé function
.
Fonctions fléchées
Es2015 ajoute une nouvelle syntaxe pour la déclaration des fonctions en javascript : Les fonctions fléchées.
1const myFunction = () => {
2 console.log(this === windows ); // True
3}
4myFunction();
Rappelez vous maintenant de cet exemple vu plus haut :
1var myObject = {
2 myFunction: function() {
3 console.log(this===myObject); // affiche: "true"
4 setTimeout(function() {
5 console.log(this === myObject); // affiche: "false"
6 console.log(this === window); // affiche: "true"
7 },0);
8 }
9}
10myObject.myFunction();
Nous en avons conclu que le pointeur de this
changeait dans la fonction anonyme callback de setTimeout
pour prendre celui de windows
ou global
.
Réécrivons ce bout de code avec les nouvelles notations abordées précédement :
1var myObject = {
2 myFunction() {
3 console.log(this===myObject); // affiche: "true"
4 setTimeout(() => {
5 console.log(this === myObject); // affiche: "true"
6 console.log(this === window); // affiche: "false"
7 },0);
8 }
9}
10myObject.myFunction();
Nous constatons que les résultats sont inversés. En utilisant les fonctions fléchés comme ci-dessus nous conservons le pointeur de this
, qui correspond à l’objet myObject
.
Fonctions fléchées et .call()
Vous vous rappelez de la méthode .call()
?
Etudions son comportement avec les fonctions fléchées.
1const myObject = {};
2
3const myFunction = () => {
4 console.log(this === myObject);
5};
6
7myFunction(); // False
8
9myFunction.call(myObject); // False
Nous avons déclaré un objet quelconque myObject
.
Nous souhaitons déplacer le pointeur de this
vers l’objet myObject
avec la méthode .call()
comme nous l’avons vu avec les fonctions déclarée avec le mot clé function
.
Et contre toute attente, nous faisons le constat que cela n’est pas possible !
Une fonction fléchée est une alternative compacte aux expressions de fonctions traditionnelles. elles ne peuvent pas être utilisé cependant dans toutes les situations.
this
etsuper
dans leur corps ne peuvent pas se lier à leur parent, nous ne devons donc pas les utiliser comme méthode d’un objet.Les fonctions fléchées n’ont pas accès au mot clé :
new.target
.Les fonctions fléchées ne peuvent pas être utilisées par les méthodes
call
,apply
etbin
.Les fonctions fléchées ne peuvent pas être utilisée comme constructeur.
Les fonctions fléchées ne peuvent pas utiliser
yield
dans leur corps.
Les propriétés d’instance dans ES2017
Avant la mise en place de ES2017, si vous vouliez ajouter une propriété à une classe, nous devions l’ajouter dans le constructeur comme suit :
1class MyClass {
2 constructor() {
3 this.myProperty = 10;
4 }
5}
6
7const myInstance = new MyClass();
8console.log(myInstance.myProperty); // 10
C’était extrêmement verbeux, et pouvait rendre complexe la lecture du constructeur. Maintenant, nous pouvons déclarer directement les propriétés en dehors du constructeur :
1class MyClass {
2 myProperty = 10;
3}
4
5const myInstance = new MyClass();
6console.log(myInstance.myProperty); // 10
Mais cela entraine quelques implications particuliaire spécialement autour des fonctions. En effet grâce à cette implémentation les méthodes d’une classe peuvent être de la forme d’une fonction fléchée et être considéré comme étant membre de la classe :
1class MyClass {
2
3 myFunction = () => {
4 console.log( this instanceof MyClass); // True
5 };
6}
7
8const myInstance = new MyClass();
9const myFunction = myInstance.myFunction;
10
11myFunction();
Et ainsi, même en créant un alias de la méthode dans une variable, elle sera toujours considéré comme étant membre de l’instance de la classe qui l’a initié, comme cela fonctionne dans des langage comme java, C# ou C++.