JavaScript est une drôle de langue. Bien qu'inspiré de Smalltalk, il utilise une syntaxe de type C. Il combine des aspects des paradigmes de programmation procédurale, fonctionnelle et orientée objet (POO). Il a nombreux , souvent redondantes, pour résoudre presque tous les problèmes de programmation imaginables et n’ont pas d’opinion prononcée sur ceux qui sont préférés. Il est typé faiblement et dynamiquement, avec une approche mazelike de la coercition de type qui trébuche même les développeurs expérimentés.
JavaScript a également ses verrues, ses pièges et ses fonctionnalités douteuses. Les nouveaux programmeurs se débattent avec certains de ses concepts les plus difficiles - pensez à l'asynchronisme, aux fermetures et au levage. Les programmeurs ayant de l'expérience dans d'autres langages supposent raisonnablement que les choses avec des noms et des apparences similaires fonctionneront de la même manière en JavaScript et sont souvent fausses. Les tableaux ne sont pas vraiment des tableaux; quel est le problème this
, qu'est-ce qu'un prototype et que fait new
faire réellement?
Le pire contrevenant est de loin le nouveau dans la dernière version de JavaScript, ECMAScript 6 (ES6): Des classes . Certaines des discussions autour des cours sont franchement alarmantes et révèlent un malentendu profondément enraciné sur le fonctionnement réel de la langue:
«JavaScript est enfin un réel langage orienté objet maintenant qu'il a des classes! »
Ou:
'Les cours nous libèrent de la réflexion sur le modèle d'héritage cassé de JavaScript.'
Ou même:
'Les cours constituent une approche plus sûre et plus simple pour créer des types en JavaScript.'
Ces déclarations ne me dérangent pas, car elles impliquent qu'il y a quelque chose qui ne va pas avec héritage prototypique ; mettons de côté ces arguments. Ces affirmations me dérangent car aucune d’elles n’est vraie et elles démontrent les conséquences de l’approche JavaScript «tout pour tous» en matière de conception de langage: elle paralyse la compréhension du langage par un programmeur plus souvent qu’elle ne le permet. Avant d’aller plus loin, illustrons-le.
function PrototypicalGreeting(greeting = 'Hello', name = 'World') { this.greeting = greeting this.name = name } PrototypicalGreeting.prototype.greet = function() { return `${this.greeting}, ${this.name}!` } const greetProto = new PrototypicalGreeting('Hey', 'folks') console.log(greetProto.greet())
class ClassicalGreeting { constructor(greeting = 'Hello', name = 'World') { this.greeting = greeting this.name = name } greet() { return `${this.greeting}, ${this.name}!` } } const classyGreeting = new ClassicalGreeting('Hey', 'folks') console.log(classyGreeting.greet())
La réponse ici est il n'y en a pas . Celles-ci font effectivement la même chose, il s’agit uniquement de savoir si la syntaxe de classe ES6 a été utilisée.
société c vs société s vs llc
Certes, le deuxième exemple est plus expressif. Pour cette seule raison, vous pourriez soutenir que class
est un bel ajout à la langue. Malheureusement, le problème est un peu plus subtil.
function Proto() { this.name = 'Proto' return this; } Proto.prototype.getName = function() { return this.name } class MyClass extends Proto { constructor() { super() this.name = 'MyClass' } } const instance = new MyClass() console.log(instance.getName()) Proto.prototype.getName = function() { return 'Overridden in Proto' } console.log(instance.getName()) MyClass.prototype.getName = function() { return 'Overridden in MyClass' } console.log(instance.getName()) instance.getName = function() { return 'Overridden in instance' } console.log(instance.getName())
La bonne réponse est qu'il imprime sur la console:
> MyClass > Overridden in Proto > Overridden in MyClass > Overridden in instance
Si vous avez répondu incorrectement, vous ne comprenez pas ce que class
est en fait. Ce n’est pas votre faute. Tout comme Array
, class
n'est pas une fonctionnalité linguistique, c'est obscurantisme syntaxique . Il essaie de cacher le modèle d'héritage prototypique et les idiomes maladroits qui l'accompagnent, et cela implique que JavaScript fait quelque chose qui n'est pas.
On vous a peut-être dit que class
a été introduit à JavaScript pour rendre les développeurs de POO classiques issus de langages comme Java plus à l'aise avec le modèle d'héritage de classe ES6. Si vous sont l'un de ces développeurs, cet exemple vous a probablement horrifié. Cela devrait. Cela montre que JavaScript est class
Le mot-clé n'est fourni avec aucune des garanties qu'une classe est censée fournir. Il montre également l'une des principales différences dans le modèle d'héritage prototype: les prototypes sont instances d'objet , ne pas les types .
La différence la plus importante entre l'héritage basé sur une classe et sur un prototype est qu'une classe définit un type qui peut être instancié au moment de l'exécution, alors qu'un prototype est lui-même une instance d'objet.
Un enfant d'une classe ES6 en est un autre type définition qui étend le parent avec de nouvelles propriétés et méthodes, qui à leur tour peuvent être instanciées au moment de l'exécution. Un enfant d'un prototype est un autre objet exemple qui délègue au parent toutes les propriétés qui ne sont pas implémentées sur l'enfant.
Note latérale: Vous vous demandez peut-être pourquoi j'ai mentionné les méthodes de classe, mais pas les méthodes prototypes. C'est parce que JavaScript n'a pas de concept de méthodes. Les fonctions sont première classe en JavaScript, et ils peuvent avoir des propriétés ou être des propriétés d'autres objets.
Un constructeur de classe crée une instance de la classe. Un constructeur en JavaScript n'est qu'une simple fonction ancienne qui renvoie un objet. La seule particularité d'un constructeur JavaScript est que, lorsqu'il est appelé avec le new
mot-clé, il affecte son prototype comme prototype de l'objet retourné. Si cela vous semble un peu déroutant, vous n’êtes pas seul - c’est le cas, et c’est en grande partie pourquoi les prototypes sont mal compris.
Pour mettre un point très fin là-dessus, un enfant d'un prototype n'est pas un copie de son prototype, ni un objet avec le même forme comme son prototype. L'enfant a une référence vivante à le prototype, et toute propriété de prototype qui n’existe pas sur l’enfant est une référence unidirectionnelle à une propriété du même nom sur le prototype.
Considérer ce qui suit:
let parent = { foo: 'foo' } let child = { } Object.setPrototypeOf(child, parent) console.log(child.foo) // 'foo' child.foo = 'bar' console.log(child.foo) // 'bar' console.log(parent.foo) // 'foo' delete child.foo console.log(child.foo) // 'foo' parent.foo = 'baz' console.log(child.foo) // 'baz'
Remarque: vous n'écririez presque jamais de code comme celui-ci dans la vraie vie - c'est une pratique terrible - mais cela démontre succinctement le principe.Dans l'exemple précédent, tandis que child.foo
était undefined
, il faisait référence à parent.foo
. Dès que nous avons défini foo
sur child
, child.foo
avait la valeur 'bar'
, mais parent.foo
a conservé sa valeur d'origine. Une fois que nous delete child.foo
il fait à nouveau référence à parent.foo
, ce qui signifie que lorsque nous modifions la valeur du parent, child.foo
fait référence à la nouvelle valeur.
Regardons ce qui vient de se passer (à des fins d'illustration plus claire, nous allons faire comme s'il s'agissait de Strings
et non de chaînes littérales, la différence n'a pas d'importance ici):
La façon dont cela fonctionne sous le capot, et surtout les particularités de new
et this
, sont un sujet pour un autre jour, mais Mozilla a un article détaillé sur la chaîne d'héritage prototype de JavaScript si vous souhaitez en savoir plus.
La clé à retenir est que les prototypes ne définissent pas un type
; ils sont eux-mêmes instances
et ils sont mutables au moment de l’exécution, avec tout ce que cela implique et implique.
Encore avec moi? Revenons à la dissection des classes JavaScript.
Nos propriétés de prototype et de classe ci-dessus ne sont pas tant «encapsulées» que «suspendues de manière précaire par la fenêtre». Nous devrions résoudre ce problème, mais comment?
Aucun exemple de code ici. La réponse est que vous ne pouvez pas.
JavaScript n'a aucun concept de confidentialité, mais il comporte des fermetures:
function SecretiveProto() { const secret = 'The Class is a lie!' this.spillTheBeans = function() { console.log(secret) } } const blabbermouth = new SecretiveProto() try { console.log(blabbermouth.secret) } catch(e) { // TypeError: SecretiveClass.secret is not defined } blabbermouth.spillTheBeans() // 'The Class is a lie!'
Comprenez-vous ce qui vient de se passer? Sinon, vous ne comprenez pas les fermetures. C'est pas grave, vraiment - ils ne sont pas aussi intimidants qu'on le prétend, ils sont super utiles et vous devriez prenez le temps d'en apprendre davantage sur eux .
class
Mot-clé?Désolé, c'est une autre question piège. Vous pouvez faire essentiellement la même chose, mais cela ressemble à ceci:
class SecretiveClass { constructor() { const secret = 'I am a lie!' this.spillTheBeans = function() { console.log(secret) } } looseLips() { console.log(secret) } } const liar = new SecretiveClass() try { console.log(liar.secret) } catch(e) { console.log(e) // TypeError: SecretiveClass.secret is not defined } liar.spillTheBeans() // 'I am a lie!'
Faites-moi savoir si cela semble plus facile ou plus clair que dans SecretiveProto
. À mon avis, c’est un peu pire - cela rompt l’utilisation idiomatique de class
des déclarations en JavaScript et cela ne fonctionne pas beaucoup comme vous vous attendez à partir de, disons, Java. Cela sera précisé par ce qui suit:
SecretiveClass::looseLips()
Faire?Découvrons-le:
try { liar.looseLips() } catch(e) { // ReferenceError: secret is not defined }
Eh bien… c'était gênant.
meilleures pratiques de conception de sites Web mobiles
Vous l'avez deviné, c'est une autre question piège: les développeurs JavaScript expérimentés ont tendance à éviter les deux lorsqu'ils le peuvent. Voici une bonne façon de procéder avec JavaScript idiomatique:
function secretFactory() { const secret = 'Favor composition over inheritance, `new` is considered harmful, and the end is near!' const spillTheBeans = () => console.log(secret) return { spillTheBeans } } const leaker = secretFactory() leaker.spillTheBeans()
Il ne s’agit pas seulement d’éviter la laideur inhérente à l’héritage ou d’imposer l’encapsulation. Pensez à ce que vous pourriez faire d'autre avec secretFactory
et leaker
ce que vous ne pourriez pas faire facilement avec un prototype ou une classe.
D'une part, vous pouvez le déstructurer car vous n'avez pas à vous soucier du contexte de this
:
const { spillTheBeans } = secretFactory() spillTheBeans() // Favor composition over inheritance, (...)
C’est plutôt sympa. En plus d'éviter new
et this
tomfoolery, cela nous permet d'utiliser nos objets de manière interchangeable avec les modules CommonJS et ES6. Cela facilite également la composition:
function spyFactory(infiltrationTarget) { return { exfiltrate: infiltrationTarget.spillTheBeans } } const blackHat = spyFactory(leaker) blackHat.exfiltrate() // Favor composition over inheritance, (...) console.log(blackHat.infiltrationTarget) // undefined (looks like we got away with it)
Clients de blackHat
n’a pas à vous soucier de l’endroit où exfiltrate
est venu de, et spyFactory
n'a pas à jouer avec Function::bind
jonglage de contexte ou propriétés profondément imbriquées. Attention, nous n'avons pas à nous soucier beaucoup de this
dans du code procédural synchrone simple, mais cela cause toutes sortes de problèmes dans le code asynchrone qu'il vaut mieux éviter.
Avec un peu de réflexion, spyFactory
pourrait être développé en un outil d'espionnage hautement sophistiqué qui pourrait gérer toutes sortes de cibles d'infiltration - ou en d'autres termes, un façade .
Bien sûr, vous pouvez également le faire avec une classe, ou plutôt un assortiment de classes, qui héritent toutes d'un abstract class
ou interface
… sauf que JavaScript n'a pas de concept d'abrégé ou d'interface.
Revenons à l'exemple de l'accueil pour voir comment nous le mettre en œuvre avec une usine:
function greeterFactory(greeting = 'Hello', name = 'World') { return { greet: () => `${greeting}, ${name}!` } } console.log(greeterFactory('Hey', 'folks').greet()) // Hey, folks!
Vous avez peut-être remarqué que ces usines deviennent de plus en plus laconiques au fur et à mesure, mais ne vous inquiétez pas, elles font la même chose. Les roues d'entraînement se détachent, les gars!
C'est déjà moins standard que le prototype ou la version de classe du même code. Deuxièmement, il réalise l'encapsulation de ses propriétés plus efficacement. En outre, il a une mémoire et une empreinte de performances plus faibles dans certains cas (cela peut ne pas sembler être le cas à première vue, mais le compilateur JIT travaille silencieusement en coulisse pour réduire la duplication et déduire les types).
C'est donc plus sûr, c'est souvent plus rapide et c'est plus facile d'écrire du code comme celui-ci. Pourquoi avons-nous à nouveau besoin de cours? Oh, bien sûr, la réutilisabilité. Que se passe-t-il si nous voulons des variantes d'accueil malheureuses et enthousiastes? Eh bien, si nous utilisons le ClassicalGreeting
classe, nous sautons probablement directement dans l’imagination d’une hiérarchie de classes. Nous savons que nous devrons paramétrer la ponctuation, nous allons donc procéder à une petite refactorisation et ajouter des enfants:
// Greeting class class ClassicalGreeting { constructor(greeting = 'Hello', name = 'World', punctuation = '!') { this.greeting = greeting this.name = name this.punctuation = punctuation } greet() { return `${this.greeting}, ${this.name}${this.punctuation}` } } // An unhappy greeting class UnhappyGreeting extends ClassicalGreeting { constructor(greeting, name) { super(greeting, name, ' :(') } } const classyUnhappyGreeting = new UnhappyGreeting('Hello', 'everyone') console.log(classyUnhappyGreeting.greet()) // Hello, everyone :( // An enthusiastic greeting class EnthusiasticGreeting extends ClassicalGreeting { constructor(greeting, name) { super(greeting, name, '!!') } greet() { return super.greet().toUpperCase() } } const greetingWithEnthusiasm = new EnthusiasticGreeting() console.log(greetingWithEnthusiasm.greet()) // HELLO, WORLD!!
C'est une bonne approche, jusqu'à ce que quelqu'un vienne et demande une fonctionnalité qui ne rentre pas proprement dans la hiérarchie et que tout cesse d'avoir un sens. Mettez une épingle dans cette pensée pendant que nous essayons d'écrire la même fonctionnalité avec des usines:
const greeterFactory = (greeting = 'Hello', name = 'World', punctuation = '!') => ({ greet: () => `${greeting}, ${name}${punctuation}` }) // Makes a greeter unhappy const unhappy = (greeter) => (greeting, name) => greeter(greeting, name, ':(') console.log(unhappy(greeterFactory)('Hello', 'everyone').greet()) // Hello, everyone :( // Makes a greeter enthusiastic const enthusiastic = (greeter) => (greeting, name) => ({ greet: () => greeter(greeting, name, '!!').greet().toUpperCase() }) console.log(enthusiastic(greeterFactory)().greet()) // HELLO, WORLD!!
Il n’est pas évident que ce code soit meilleur, même s’il est un peu plus court. En fait, vous pourriez dire qu’il est plus difficile à lire, et qu’il s’agit peut-être d’une approche obtuse. Ne pourrions-nous pas simplement avoir un unhappyGreeterFactory
et un enthusiasticGreeterFactory
?
Ensuite, votre client arrive et dit: «J'ai besoin d'un nouveau greeter qui est mécontent et veut que toute la salle le sache!»
console.log(enthusiastic(unhappy(greeterFactory))().greet()) // HELLO, WORLD :(
Si nous devions utiliser plus d'une fois cet accueil enthousiaste et malheureux, nous pourrions nous faciliter la tâche:
const aggressiveGreeterFactory = enthusiastic(unhappy(greeterFactory)) console.log(aggressiveGreeterFactory('You're late', 'Jim').greet())
Il existe des approches de ce style de composition qui fonctionnent avec des prototypes ou des classes. Par exemple, vous pouvez repenser UnhappyGreeting
et EnthusiasticGreeting
comme décorateurs . Il faudrait encore plus de passe-partout que l'approche de style fonctionnel utilisée ci-dessus, mais c'est le prix que vous payez pour la sécurité et l'encapsulation de réel Des classes.
Le problème, c'est qu'en JavaScript, vous n'obtenez pas cette sécurité automatique. Des frameworks JavaScript qui mettent l'accent sur class
l'usage fait beaucoup de «magie» pour dissimuler ce genre de problèmes et oblige les classes à se comporter. Jetez un œil à Polymer’s ElementMixin
code source quelque temps, je te défie. Ce sont des niveaux arcanes JavaScript archi-magiciens, et je veux dire cela sans ironie ni sarcasme.
Bien sûr, nous pouvons résoudre certains des problèmes évoqués ci-dessus avec Object.freeze
ou Object.defineProperties
à un effet plus ou moins grand. Mais pourquoi imiter le formulaire sans la fonction, tout en ignorant les outils JavaScript Est-ce que nous fournir nativement que nous pourrions ne pas trouver dans des langages comme Java? Utiliseriez-vous un marteau étiqueté «tournevis» pour enfoncer une vis, alors que votre boîte à outils avait comme véritable tournevis assis juste à côté?
combien vaut l'industrie de la musique
Les développeurs JavaScript mettent souvent l'accent sur les bonnes parties du langage, à la fois familièrement et en référence à le livre du même nom . Nous essayons d'éviter les pièges posés par ses choix de conception de langage plus discutables et de nous en tenir aux parties qui nous permettent d'écrire du code propre, lisible, minimisant les erreurs et réutilisable.
Il y a des arguments raisonnables sur les parties de JavaScript admissibles, mais j'espère vous avoir convaincu que class
n'en fait pas partie. À défaut, j'espère que vous comprenez que l'héritage en JavaScript peut être un désordre déroutant et que class
ne le corrige ni ne vous évite d'avoir à comprendre les prototypes. Un crédit supplémentaire si vous avez compris les indices selon lesquels les modèles de conception orientés objet fonctionnent correctement sans classes ni héritage ES6.
Je ne vous dis pas d’éviter class
entièrement. Parfois, vous avez besoin d'héritage et class
fournit une syntaxe plus claire pour ce faire. En particulier, class X extends Y
est beaucoup plus agréable que l'ancienne approche de prototype. En plus de cela, de nombreux frameworks frontaux populaires encouragent son utilisation et vous devriez probablement éviter d'écrire du code non standard étrange par principe. Je n'aime pas où ça va.
Dans mes cauchemars, toute une génération de bibliothèques JavaScript est écrite en utilisant class
, dans l'espoir qu'elle se comporte de la même manière que d'autres langages populaires. De nouvelles classes entières de bugs (jeu de mots) sont découvertes. Les anciens sont ressuscités qui auraient facilement pu être laissés dans le cimetière de JavaScript malformé si nous n’avions pas été négligemment tombés dans le class
piège. Les développeurs JavaScript expérimentés sont en proie à ces monstres, car ce qui est populaire n'est pas toujours ce qui est bon.
Finalement, nous abandonnons tous dans la frustration et commençons à réinventer les roues dans Rust, Go, Haskell ou qui sait quoi d'autre, puis à compiler vers Wasm pour le Web, et de nouveaux frameworks et bibliothèques Web prolifèrent dans l'infini multilingue.
Cela me tient vraiment éveillé la nuit.
ES6 est la dernière implémentation stable d'ECMAScript, le standard ouvert sur lequel JavaScript est basé. Il ajoute un certain nombre de nouvelles fonctionnalités au langage, notamment un système de module officiel, des variables et des constantes à portée de bloc, des fonctions fléchées et de nombreux autres nouveaux mots-clés, syntaxes et objets intégrés.
ES6 (ES2015) est la norme la plus récente qui est stable et entièrement mise en œuvre (à l'exception des appels de queue appropriés et de certaines nuances) dans les dernières versions des principaux navigateurs et autres environnements JS. ES7 (ES2016) et ES8 (ES2017) sont également des spécifications stables, mais la mise en œuvre est assez mitigée.
JavaScript a un support solide pour la programmation orientée objet, mais il utilise un modèle d'héritage différent (prototypique) par rapport aux langages OO les plus populaires (qui utilisent l'héritage classique). Il prend également en charge les styles de programmation procéduraux et fonctionnels.
Dans ES6, le mot-clé «classe» et les fonctionnalités associées constituent une nouvelle approche pour créer des prototypes de constructeurs. Ce ne sont pas de vraies classes d'une manière qui serait familière aux utilisateurs de la plupart des autres langages orientés objet.
On peut implémenter l'héritage dans JavaScript ES6 via les mots-clés «classe» et «étend». Une autre approche consiste à utiliser l'idiome de fonction «constructeur» plus l'affectation de fonctions et de propriétés statiques au prototype du constructeur.
Dans l'héritage prototypique, les prototypes sont des instances d'objet auxquelles les instances enfants délèguent des propriétés non définies. En revanche, les classes dans l'héritage classique sont des définitions de type, à partir desquelles les classes enfants héritent des méthodes et des propriétés lors de l'instanciation.