Impossible de résoudre le mystère des fonctions en Javascript

16

J'essaie de comprendre les coulisses de Javascript et je suis un peu coincé dans la compréhension de la création d'objets intégrés, spécialement Object and Function et la relation entre eux.

Quand j'ai lu que tous les objets intégrés comme Array, String, etc. sont une extension (héritée) d'Object, j'ai supposé que Object était le premier objet intégré créé et le reste des objets en héritait. Mais cela n'a pas de sens lorsque vous apprenez que les objets ne peuvent être créés que par des fonctions, mais alors les fonctions ne sont rien d'autre que des objets de fonction. Cela a commencé à ressembler à un dilemme de poule et de poulet.

L'autre chose extrêmement déroutante est que si console.log(Function.prototype)j'imprime une fonction, mais lorsque j'imprime, console.log(Object.prototype)elle imprime un objet. Pourquoi Function.prototypeune fonction alors qu'elle était censée être un objet?

De plus, selon la documentation de Mozilla, chaque javascript functionest une extension d' Functionobjet, mais quand vous console.log(Function.prototype.constructor)le faites à nouveau, c'est une fonction. Maintenant, comment pouvez-vous utiliser quelque chose pour le créer vous-même (Mind = blown).

La dernière chose Function.prototypeest une fonction mais je peux accéder à la constructorfonction en utilisant Function.prototype.constructorce que cela signifie Function.prototypeest une fonction qui renvoie l' prototypeobjet

Umair Abid
la source
puisqu'une fonction est un objet, cela signifie que la Function.prototypepeut être une fonction et avoir des champs internes. Donc non, vous n'exécutez pas la fonction prototype lorsque vous parcourez sa structure. Enfin, n'oubliez pas qu'il existe un moteur interprétant Javascript, donc l'objet et la fonction sont probablement créés dans le moteur et non à partir de Javascript et de références spéciales comme Function.prototypeet Object.prototypepourraient simplement être interprétés de manière spéciale par le moteur.
Walfrat
1
À moins que vous ne cherchiez à implémenter un compilateur JavaScript conforme aux normes, vous n'avez vraiment pas à vous soucier de ce genre de choses. Si vous cherchez à faire quelque chose d'utile, vous êtes hors cours.
Jared Smith
5
Pour info la phrase habituelle en anglais pour "le dilemme de la poule et du poulet" est "le problème du poulet et des œufs", à savoir "qui est venu en premier, le poulet ou l'œuf?" (Bien sûr, la réponse est l'œuf. Les animaux ovipares ont été autour pendant des millions d'années avant les poulets.)
Eric Lippert

Réponses:

32

J'essaie de comprendre les coulisses de Javascript et je suis un peu coincé dans la compréhension de la création d'objets intégrés, spécialement Object and Function et la relation entre eux.

C'est compliqué, il est facile de se méprendre, et un grand nombre de livres Javascript pour débutants se trompent, alors ne faites pas confiance à tout ce que vous lisez.

J'ai été l'un des implémenteurs du moteur JS de Microsoft dans les années 1990 et membre du comité de normalisation, et j'ai fait un certain nombre d'erreurs en préparant cette réponse. (Même si je n'ai pas travaillé là-dessus depuis plus de 15 ans, je peux peut-être être pardonné.) C'est un truc délicat. Mais une fois que vous comprenez l'héritage des prototypes, tout devient logique.

Quand j'ai lu que tous les objets intégrés comme Array, String, etc. sont une extension (héritée) d'Object, j'ai supposé que Object était le premier objet intégré créé et le reste des objets en héritait.

Commencez par jeter tout ce que vous savez sur l'héritage basé sur les classes. JS utilise l'héritage basé sur un prototype.

Ensuite, assurez-vous d'avoir une définition très claire dans votre tête de ce que signifie «héritage». Les gens habitués aux langages OO comme C # ou Java ou C ++ pensent que l'héritage signifie le sous-typage, mais l'héritage ne signifie pas le sous-typage. L'héritage signifie que les membres d'une chose sont également membres d'une autre chose . Cela ne signifie pas nécessairement qu'il existe une relation de sous-typage entre ces choses! Tant de malentendus dans la théorie des types sont le résultat de personnes ne réalisant pas qu'il y a une différence.

Mais cela n'a pas de sens lorsque vous apprenez que les objets ne peuvent être créés que par des fonctions, mais alors les fonctions ne sont rien d'autre que des objets de fonction.

C'est tout simplement faux. Certains objets ne sont pas créés en appelant new Fune fonction F. Certains objets sont créés par le runtime JS à partir de rien du tout. Il y a des œufs qui n'ont été pondus par aucun poulet . Ils ont juste été créés par le runtime au démarrage.

Disons quelles sont les règles et peut-être que cela aidera.

  • Chaque instance d'objet a un objet prototype.
  • Dans certains cas, ce prototype peut être null.
  • Si vous accédez à un membre sur une instance d'objet et que l'objet n'a pas ce membre, alors l'objet passe à son prototype ou s'arrête si le prototype est nul.
  • Le prototypemembre d'un objet n'est généralement pas le prototype de l'objet.
  • Au contraire, le prototypemembre d'un objet fonction F est l'objet qui deviendra le prototype de l'objet créé par new F().
  • Dans certaines implémentations, les instances obtiennent un __proto__membre qui donne vraiment leur prototype. (Ceci est désormais obsolète. Ne comptez pas dessus.)
  • Les objets fonction reçoivent un tout nouvel objet par défaut affecté prototypelors de leur création.
  • Le prototype d'un objet fonction est, bien sûr Function.prototype.

Résumons.

  • Le prototype de ObjectestFunction.prototype
  • Object.prototype est l'objet prototype d'objet.
  • Le prototype de Object.prototypeestnull
  • Le prototype de Functionest Function.prototype- c'est l'une des rares situations où Function.prototypeest en fait le prototype de Function!
  • Function.prototype est l'objet prototype de la fonction.
  • Le prototype de Function.prototypeestObject.prototype

Supposons que nous faisons une fonction Foo.

  • Le prototype de Foois Function.prototype.
  • Foo.prototype est l'objet prototype Foo.
  • Le prototype de Foo.prototypeis Object.prototype.

Supposons que nous disions new Foo()

  • Le prototype du nouvel objet est Foo.prototype

Assurez-vous que cela a du sens. Dessinons-le. Les ovales sont des instances d'objets. Les arêtes __proto__signifient soit "le prototype de", soit prototype"la prototypepropriété de".

entrez la description de l'image ici

Le runtime n'a qu'à créer tous ces objets et à affecter leurs différentes propriétés en conséquence. Je suis sûr que vous pouvez voir comment cela se ferait.

Voyons maintenant un exemple qui teste vos connaissances.

function Car(){ }
var honda = new Car();
print(honda instanceof Car);
print(honda.constructor == Car);

Qu'est-ce que cette impression?

Eh bien, qu'est-ce que cela instanceofsignifie? honda instanceof Carsignifie "est Car.prototypeégal à n'importe quel objet sur hondala chaîne prototype de?"

Oui, ça l'est. hondale prototype est Car.prototype, donc nous avons terminé. Cela s'imprime vrai.

Et le deuxième?

honda.constructorn'existe pas donc nous consultons le prototype, qui l'est Car.prototype. Lorsque l' Car.prototypeobjet a été créé, il lui a été automatiquement attribué une propriété constructorégale à Car, c'est donc vrai.

Et maintenant?

var Animal = new Object();
function Reptile(){ }
Reptile.prototype = Animal;
var lizard = new Reptile();
print(lizard instanceof Reptile);
print(lizard.constructor == Reptile);

Qu'est-ce que ce programme imprime?

Encore une fois, lizard instanceof Reptilesignifie "est Reptile.prototypeégal à n'importe quel objet sur lizardla chaîne prototype de?"

Oui, ça l'est. lizardle prototype est Reptile.prototype, donc nous avons terminé. Cela s'imprime vrai.

Et maintenant

print(lizard.constructor == Reptile);

Vous pourriez penser que cela s'imprime également, car a lizardété construit avec new Reptilemais vous vous trompez. Raisonnez-le.

  • A lizardune constructorpropriété? Non. Par conséquent, nous examinons le prototype.
  • Le prototype de lizardis Reptile.prototype, qui est Animal.
  • A Animalune constructorpropriété? Non. Donc on regarde son prototype.
  • Le prototype de Animalest Object.prototype, et Object.prototype.constructorest créé par le runtime et est égal à Object.
  • Donc, cela est faux.

Nous aurions dû dire Reptile.prototype.constructor = Reptile;à un moment donné, mais nous ne nous en souvenions pas!

Assurez-vous que tout a du sens pour vous. Dessinez des cases et des flèches si cela prête à confusion.

L'autre chose extrêmement déroutante est que si console.log(Function.prototype)j'imprime une fonction, mais lorsque j'imprime, console.log(Object.prototype)elle imprime un objet. Pourquoi Function.prototypeune fonction alors qu'elle était censée être un objet?

Le prototype de fonction est défini comme une fonction qui, lorsqu'elle est appelée, retourne undefined. Nous savons déjà que Function.prototypec'est le Functionprototype, curieusement. C'est donc Function.prototype()légal et quand vous le faites, vous undefinedrevenez. C'est donc une fonction.

Le Objectprototype n'a pas cette propriété; ce n'est pas appelable. Ce n'est qu'un objet.

quand vous console.log(Function.prototype.constructor)c'est encore une fonction.

Function.prototype.constructorest juste Function, évidemment. Et Functionc'est une fonction.

Maintenant, comment pouvez-vous utiliser quelque chose pour le créer vous-même (Mind = blown).

Vous pensez trop à cela . Tout ce qui est requis, c'est que le runtime crée un tas d'objets au démarrage. Les objets ne sont que des tables de recherche associant des chaînes à des objets. Lorsque l'exécution démarre, tout ce qu'il a à faire est de créer quelques dizaines d' objets en blanc, puis commencer à attribuer le prototype, __proto__, constructoret ainsi sur les propriétés de chaque objet jusqu'à ce qu'ils fassent le graphique qu'ils doivent faire.

Il sera utile de prendre le diagramme que je vous ai donné ci-dessus et d'y ajouter des constructorbords. Vous verrez rapidement qu'il s'agit d'un graphe d'objet très simple et que le runtime n'aura aucun problème à le créer.

Un bon exercice serait de le faire vous-même. Ici, je vais commencer. Nous utiliserons my__proto__pour signifier "l'objet prototype de" et myprototypepour signifier "la propriété prototype de".

var myobjectprototype = new Object();
var myfunctionprototype = new Object();
myfunctionprototype.my__proto__ = myobjectprototype;
var myobject = new Object();
myobject.myprototype = myobjectprototype;

Etc. Pouvez-vous remplir le reste du programme pour construire un ensemble d'objets qui ont la même topologie que les "vrais" objets intégrés Javascript? Si vous le faites, vous constaterez que c'est extrêmement facile.

Les objets en JavaScript ne sont que des tables de recherche qui associent des chaînes à d'autres objets . C'est ça! Il n'y a pas de magie ici. Vous vous faites nouer parce que vous imaginez des contraintes qui n'existent pas réellement, comme si chaque objet devait être créé par un constructeur.

Les fonctions ne sont que des objets qui ont une capacité supplémentaire: être appelés. Parcourez donc votre petit programme de simulation et ajoutez une .mycallablepropriété à chaque objet qui indique s'il peut être appelé ou non. C'est aussi simple que ça.

Eric Lippert
la source
9
Enfin, une explication courte, concise et facile à comprendre de JavaScript! Excellent! Comment pourrait-on confondre l'un de nous? :) Bien que très sérieusement, le dernier élément sur les objets qui sont des tables de recherche est vraiment la clé. Il y a une méthode à la folie --- mais c'est toujours de la folie ...
Greg Burghardt
4
@GregBurghardt: Je suis d'accord que cela semble complexe au premier abord, mais la complexité est la conséquence de règles simples. Chaque objet a un __proto__. Le __proto__prototype de l'objet est nul. Le __proto__de new X()est X.prototype. Tous les objets fonction ont le prototype de fonction à l' __proto__exception du prototype de fonction lui-même. Objectet Functionet le prototype de fonction sont des fonctions. Ces règles sont toutes simples et déterminent la topologie du graphe des objets initiaux.
Eric Lippert
6

Vous avez déjà beaucoup d'excellentes réponses, mais je veux juste donner une réponse courte et claire à votre réponse sur la façon dont tout cela fonctionne, et cette réponse est:

LA MAGIE!!!

Vraiment, c'est tout.

Les personnes qui mettent en œuvre les moteurs d'exécution ECMAScript doivent mettre en œuvre les règles de ECMAScript, mais pas respecter en leur sein de leur mise en œuvre.

La spécification ECMAScript dit que A hérite de B mais B est une instance de A? Aucun problème! Créez d'abord A avec un pointeur prototype de NULL, créez B comme une instance de A, puis fixez le pointeur prototype de A pour pointer vers B par la suite. Peasy facile.

Vous dites, mais attendez, il n'y a aucun moyen de changer le pointeur prototype dans ECMAScript! Mais voici le problème: ce code ne fonctionne pas sur le moteur ECMAScript, ce code est le moteur ECMAScript. Il ne accès à des objets internes que le code ECMAScript fonctionnement sur le moteur n'a pas. En bref: il peut faire ce qu'il veut.

Soit dit en passant, si vous le voulez vraiment, vous n'avez qu'à le faire une fois: par la suite, vous pouvez par exemple vider votre mémoire interne et charger ce vidage chaque fois que vous démarrez votre moteur ECMAScript.

Notez que tout cela s'applique toujours, même si le moteur ECMAScript lui-même a été écrit en ECMAScript (comme c'est en fait le cas pour Mozilla Narcissus, par exemple). Même dans ce cas, le code ECMAScript qui implémente le moteur a toujours un accès complet au moteur qu'il implémente , bien qu'il n'ait bien sûr pas accès au moteur sur lequel il tourne .

Jörg W Mittag
la source
3

D'après la spécification ECMA 1

ECMAScript ne contient pas de classes appropriées telles que celles de C ++, Smalltalk ou Java, mais prend plutôt en charge les constructeurs qui créent des objets en exécutant du code qui alloue du stockage pour les objets et initialise tout ou partie d'entre eux en attribuant des valeurs initiales à leurs propriétés. Toutes les fonctions, y compris les constructeurs, sont des objets, mais tous les objets ne sont pas des constructeurs.

Je ne vois pas comment cela pourrait être plus clair !!! </sarcasm>

Plus bas, nous voyons:

Prototype Un prototype est un objet utilisé pour implémenter l'héritage de structure, d'état et de comportement dans ECMAScript. Lorsqu'un constructeur crée un objet, cet objet référence implicitement le prototype associé au constructeur dans le but de résoudre les références de propriété. Le prototype associé au constructeur peut être référencé par l'expression de programme constructor.prototype, et les propriétés ajoutées au prototype d'un objet sont partagées, par héritage, par tous les objets partageant le prototype.

On voit donc qu'un prototype est un objet, mais pas nécessairement un objet fonction.

En outre, nous avons ce titbit intéressant

http://www.ecma-international.org/ecma-262/8.0/index.html#sec-object-objects

Le constructeur Object est l'objet intrinsèque% Object% et la valeur initiale de la propriété Object de l'objet global.

et

Le constructeur Function est l'objet intrinsèque% Function% et la valeur initiale de la propriété Function de l'objet global.

Ewan
la source
Maintenant c'est le cas. ECMA6 vous permet de créer des classes et d'instancier des objets à partir de celles-ci.
ncmathsadist le
2
Les classes @ncmathsadist ES6 ne sont qu'un sucre syntaxique, la sémantique est la même.
Hamza Fatmi
1
Votre sarcasmsurnom sinon, ce texte est vraiment assez opaque pour un débutant.
Robert Harvey
vrai, j'ajouterai plus tard, j'ai besoin de creuser
Ewan
1
euh? pour signaler que ce n'est pas clair d'après le doc
Ewan
1

Les types suivants englobent chaque valeur dans JavaScript:

  • boolean
  • number
  • undefined(qui inclut la valeur unique undefined)
  • string
  • symbol («choses» uniques abstraites qui sont comparées par référence)
  • object

Chaque objet (c'est-à-dire tout) en JavaScript a un prototype, qui est une sorte d'objet.

Le prototype contient des fonctions, qui sont aussi une sorte d'objet 1 .

Les objets ont également un constructeur, qui est une fonction, et donc une sorte d'objet.

imbriqué

Tout est récursif, mais l'implémentation est capable de le faire de manière automatique car, contrairement au code JavaScript, il peut créer des objets sans avoir à appeler des fonctions JavaScript (car les objets ne sont que de la mémoire que l'implémentation contrôle).

La plupart des systèmes d'objets dans de nombreux langages typés dynamiquement sont circulaires 2 comme celui-ci. Par exemple, en Python, les classes sont des objets, et la classe des classes l'est type, donc typeest donc une instance d'elle-même.

La meilleure idée est d'utiliser simplement les outils fournis par la langue et de ne pas trop penser à la façon dont ils y sont arrivés.

1 Les fonctions sont assez spéciales car elles sont appelables et ce sont les seules valeurs pouvant contenir des données opaques (leur corps et éventuellement une fermeture).

2 Il s'agit en fait plutôt d'un ruban tortueux et ramifié plié en arrière sur lui-même, mais "circulaire" est assez proche.

Challenger5
la source