Maison > Article > interface Web > Compréhension approfondie des mécanismes sous-jacents de JS tels que les types de données JS, la précompilation et le contexte d'exécution
JavaScript se compose de trois parties : le modèle objet de document DOM, le modèle objet de navigateur BOM et son noyau ECMAScript. Cet article apporte des connaissances sur les principes sous-jacents de JavaScript et espère être utile à tout le monde.
JavaScript est un langage de script interprété littéralement, dynamique, faiblement typé et basé sur des prototypes. JavaScript est enraciné dans le navigateur Web que nous utilisons et son interprète est le moteur JavaScript du navigateur. Ce langage de script largement utilisé côté client a d'abord été utilisé pour gérer certaines opérations de validation d'entrée qui étaient auparavant gérées par des langages côté serveur. Avec le développement de l'ère Web, JavaScript a continué de croître et est devenu un langage de programmation entièrement fonctionnel. . Son utilisation ne se limite plus à la simple validation des données, mais a la capacité d'interagir avec presque tous les aspects de la fenêtre du navigateur et de son contenu. C'est à la fois un langage très simple et un langage extrêmement complexe. Si nous voulons vraiment maîtriser JavaScript, nous devons avoir une compréhension approfondie de certains de ses principes de conception sous-jacents. Cet article fera référence aux séries de livres « JavaScript Advanced Programming » et « JS You Don't Know » pour expliquer certaines connaissances sous-jacentes sur JavaScript.
Types de données
Selon la méthode de stockage, les types de données JavaScript peuvent être divisés en deux types, les types de données primitifs (valeurs d'origine) et les types de données de référence (valeurs de référence).
Il existe actuellement six types de données primitifs, dont Nombre, Chaîne, Booléen, Null, Non défini et Symbole (ES6). Ces types sont des valeurs réelles stockées dans des variables qui peuvent être directement manipulées. Les types de données primitifs sont stockés sur la pile et la taille des données est déterminée. Ils sont stockés directement par valeur, afin qu'ils soient accessibles directement par valeur.
Le type de données de référence est Object. En JavaScript, tout, à l'exception des types de données primitifs, sont des types Object, y compris les tableaux, les fonctions, les expressions régulières, etc., qui sont tous des objets. Un type de référence est un objet stocké dans la mémoire tas et une variable est une adresse de référence stockée dans la mémoire pile qui pointe vers un objet dans la mémoire tas. Lorsqu'une variable est définie et initialisée à une valeur de référence, si elle est affectée à une autre variable, les deux variables sauvegardent la même adresse et pointent vers le même espace mémoire dans la mémoire tas. Si vous modifiez la valeur d'un type de données de référence via l'une des variables, l'autre variable changera également en conséquence. , Pour le type de données d'origine, sauf que NULL est spécial
(NULL sera considéré comme une référence d'objet vide) , d'autres peuvent utiliser Typeof pour porter un jugement précis :
"Valeur de retour" | 'string' | |
"type de vrai" type d'indéfini |
'indéfini ' |
|
type de variable inconnue ( |
variable non définie)
|
|
type de symbole()
|
'symbole' |
|
typede fonction() {} |
'fonction' |
|
typede {} |
'objet' |
|
typede [] |
'objet' |
|
typede(/[0-9,a-z]/) |
'objet' |
( peut également être attribuée manuellement à undéfini) En JavaScript, l'opérateur d'égalité == ne peut pas faire la distinction entre nul et indéfini. Leur test d'égalité devrait renvoyer vrai. Pour distinguer avec précision deux valeurs, vous devez utiliser l'opérateur de congruence ===.
Pour les types de données de référence, à l'exception de la fonction, qui a une conception de méthode spéciale et peut être jugée avec précision avec typeof, tous les autres renvoient le type d'objet. Nous pouvons utiliser instanceof pour juger les valeurs du type de référence. instanceof détectera si un objet A est une instance d'un autre objet B. Au niveau inférieur, il vérifiera si l'objet B existe sur la chaîne prototype de l'objet A(Les instances et les chaînes prototypes seront discutées plus tard dans l'article) . Renvoie vrai s'il est présent, faux s'il est absent.
Expression | Valeur de retour |
[1,2,3] instance de Array |
fonction 'vrai'
|
foo(){ } instanceof Function
|
'true'
|
/[0-9,a-z]/ instanceof RegExp
|
'true'
|
nouvelle instance Date() de Date
|
'true'
|
{name:"Alan",age:"22"} instanceof Object
|
'true'
|
Expression |
Valeur de retour |
[1,2,3] instance d'Objet |
'tru Fonction e' |
foo(){ } instanceof Object |
'true' |
/[0-9,a-z]/ instanceof Object |
'true' |
nouvelle instance Date() d'Objet |
'true' |
Bien sûr, il existe une méthode plus puissante qui peut déterminer avec précision n'importe quel type de données dans n'importe quel JavaScript, et c'est la méthode Object.prototype.toString.call(). Dans ES5, tous les objets (objets natifs et objets hôtes) ont une propriété interne [[Class]], dont la valeur est une chaîne qui enregistre le type de l'objet. Inclut actuellement "Array", "Boolean", "Date", "Error", "Function", "Math", "Number", "Object", "RegExp", "String", "Arguments", "JSON", "Symbole". Cette propriété interne peut être visualisée via la méthode Object.prototype.toString(), il n'y a pas d'autre moyen.
Lorsque la méthode Object.prototype.toString() est appelée, les étapes suivantes seront effectuées : 1. Récupérer la valeur de l'attribut [[Class]] de cet objet(Je vais parler de cet objet plus tard dans l'article) . 2. Placez la valeur entre les deux chaînes "[object" et "]" et concaténez-les. 3. Renvoyez la chaîne épissée.
Lorsque la valeur de this est nulle, la méthode Object.prototype.toString() renvoie directement "[object Null]". Lorsque la valeur de this n'est pas définie, "[objet non défini]" est renvoyé directement.
Expression |
Valeur de retour |
Object.prototype.toString.call(123) |
[ Numéro d'objet] |
Object.prototype.toString.call("abc") |
[object String] |
Object.prototype.toString.call(true) |
[object Boolean ] |
Object.prototype.toString.call(null) |
[object Null] |
Object.prototype.toString.call(undefined ) | [objet non défini] |
Object.prototype.toString.call(Symbol()) |
[objet Symbole] |
Object.prototype.toString . call(function foo(){}) |
[object Function] |
Object.prototype.toString.call([1,2,3]) |
[ tableau d'objets] |
Object.prototype.toString.call({name:"Alan" }) |
[object Object] |
Object.pro totype . toString.call(new Date()) |
[object Date] |
Object.prototype.toString.call(RegExp()) |
[object RegExp] |
Object.prototype.toString.call(window.JSON) |
[objet JSON] |
Object.prototype.toString.call(Math) |
[objet Math] |
La méthode call() peut changer le point de cela lors de l'appel de la méthode Object.prototype.toString() afin qu'elle pointe vers l'objet que nous transmettons, afin que nous puissions obtenir l'attribut [[Class]] de l'objet que nous transmettons. pass in( Le même effet peut être obtenu en utilisant Object.prototype.toString.apply().
Les types de données JavaScript peuvent également être convertis. La conversion de type de données est divisée en deux méthodes : la conversion de type explicite et la conversion de type implicite.
Les méthodes qui peuvent être appelées pour la conversion du type d'affichage incluent Boolean(), String(), Number(), parseInt(), parseFloat() et toString() (null et valeurs non définies je n'ai pas cette méthode) , leurs utilisations respectives sont claires en un coup d'œil, je ne les présenterai donc pas une par une ici.
Étant donné que JavaScript est un langage faiblement typé, lors de l'utilisation d'opérateurs arithmétiques, les types de données des deux côtés de l'opérateur peuvent être arbitraires. Il n'est pas nécessaire de spécifier le même type comme dans le langage Java ou C, le moteur. la conversion de type sera automatiquement implicite pour eux. La conversion de type implicite n'est pas aussi intuitive que la conversion de type explicite. Il existe trois méthodes de conversion principales :
1. Convertissez la valeur en une valeur primitive : toPrimitive()
2. Convertissez la valeur en un nombre : toNumber()
3 . Convertir la valeur en nombre Convertir la valeur en chaîne : toString()
De manière générale, lors de l'ajout de nombres et de chaînes, les nombres seront convertis en chaînes lors de l'exécution d'un jugement de valeur de vérité (comme if, ||, &&) , le paramètre sera converti en valeur booléenne ; lorsque des opérations de comparaison, des opérations arithmétiques ou des opérations d'incrémentation et de décrémentation automatiques sont effectuées, les paramètres seront convertis en valeurs numériques lorsque l'objet nécessite une conversion de type implicite, la méthode toString() de l'objet ; ou La valeur de retour de la méthode valueOf().
À propos de NaN :
NaN est une valeur numérique spéciale, représentant une valeur non numérique. Premièrement, toute opération arithmétique impliquant NaN renverra NaN. Deuxièmement, NaN n’est égal à aucune valeur, y compris NaN lui-même. ECMAScript définit une fonction isNaN(), qui peut être utilisée pour tester si un paramètre est « non numérique ». Il tente d'abord de convertir implicitement l'argument en valeur numérique, renvoyant vrai s'il ne peut pas être converti en valeur numérique.
Nous pouvons d'abord utiliser typeof pour déterminer s'il s'agit d'un type Number, puis utiliser isNaN pour déterminer si les données actuelles sont NaN.
À propos des chaînes :
Les chaînes en JavaScript sont immuables Une fois les chaînes créées, leurs valeurs ne peuvent pas être modifiées. Pour modifier la chaîne détenue par une variable, détruisez d'abord la chaîne d'origine, puis remplissez la variable avec une autre chaîne contenant la nouvelle valeur. Ce processus se déroule en arrière-plan, c'est pourquoi certains navigateurs plus anciens sont très lents lors de la concaténation de chaînes.
En fait, afin de faciliter le fonctionnement des valeurs de type de base, ECMAScript fournit également 3 types de référence spéciaux : Boolean, Number et String. Les types de données primitifs n'ont ni propriétés ni méthodes. Lorsque nous appelons des méthodes sur les valeurs de type primitif pour les lire, le processus d'accès sera en mode lecture et un objet de type wrapper primitif correspondant sera créé en arrière-plan, nous permettant de le faire. Appelez quelques méthodes pour manipuler ces données. Ce processus est divisé en trois étapes : 1. Créer une instance du type d'emballage d'origine 2. Appeler la méthode spécifiée sur l'instance 3. Détruire l'instance.
La principale différence entre les types référence et les types wrapper primitifs est le cycle de vie de l'objet. L'objet de type wrapper primitif créé automatiquement n'existe qu'au moment de l'exécution d'une ligne de code, puis est détruit immédiatement, nous ne pouvons donc pas ajouter d'attributs. aux valeurs de type primitif au moment de l'exécution et des méthodes.
Dans le livre "JavaScript You Don't Know", l'auteur a déclaré que bien que JavaScript soit classé comme un "langage dynamique" ou "langage d'exécution interprété", il s'agit en fait d'un langage compilé. L'exécution de JavaScript est divisée en trois étapes : 1. Analyse syntaxique 2. Précompilation 3. Interprétation et exécution. L'analyse syntaxique et l'exécution de l'interprétation ne sont pas difficiles à comprendre. L'une consiste à vérifier si le code contient des erreurs de syntaxe, et l'autre est responsable de l'exécution du programme ligne par ligne. Cependant, l'étape de précompilation en JavaScript est légèrement plus compliquée.
Tout code JavaScript doit être compilé avant son exécution. Dans la plupart des cas, le processus de compilation se produit quelques microsecondes avant l'exécution du code. Lors de la phase de compilation, le moteur JavaScript partira de la portée actuelle d'exécution du code et effectuera une requête RHS sur le code pour obtenir la valeur de la variable. Puis lors de la phase d'exécution, le moteur exécutera la requête LHS et attribuera des valeurs aux variables.
Pendant la phase de compilation, une partie du travail du moteur JavaScript consiste à retrouver toutes les déclarations et à les associer à la portée appropriée. Pendant le processus de précompilation, s'il est dans la portée globale, le moteur JavaScript créera d'abord un objet global (objet GO, Global Object) dans la portée globale, et promouvra la déclaration de variable et la déclaration de fonction. La variable promue est d'abord initialisée à indéfini par défaut, et la fonction promeut l'intégralité du corps de la fonction(Si la fonction est définie sous la forme d'une expression de fonction, les règles de promotion des variables sont appliquées) , puis stockez-les dans des variables globales. La promotion des déclarations de fonctions aura priorité sur la promotion des déclarations de variables. Pour les déclarations de variables, les déclarations var répétées seront ignorées par le moteur, et les déclarations de fonction qui apparaissent plus tard peuvent écraser les déclarations de fonction précédentes (nouvelle syntaxe de déclaration de variable ES6). la situation est légèrement différente, nous n'en discuterons donc pas ici pour l'instant).
L'intérieur du corps de fonction est une portée indépendante, et l'étape de pré-compilation sera également effectuée à l'intérieur du corps de fonction. À l'intérieur du corps de la fonction, un objet actif (objet AO, objet actif) est d'abord créé, et les déclarations formelles de paramètres et de variables ainsi que les déclarations de fonction à l'intérieur du corps de fonction sont promues. sont initialisées à indéfinies. Les fonctions internes sont toujours le corps de la fonction interne lui-même, puis elles sont stockées dans l'objet actif.
Une fois la phase de compilation terminée, le code JavaScript sera exécuté. Le processus d'exécution attribue des valeurs aux variables ou aux paramètres formels en séquence. Le moteur recherchera les déclarations de variables correspondantes ou les déclarations de paramètres formels dans la portée, et s'il les trouve, il leur attribuera des valeurs. Pour le mode non strict, si une variable est affectée sans déclaration, le moteur créera automatiquement et implicitement une déclaration pour la variable dans l'environnement global. Cependant, pour le mode strict, une erreur sera signalée si une variable non déclarée reçoit une déclaration. valeur. . Étant donné que l'exécution de JavaScript est monothread, si vous obtenez la variable (requête RHS) et que vous la sortez avant que l'opération d'affectation (requête LHS) ne soit exécutée, vous obtiendrez un résultat indéfini. Parce que la variable n’a pas reçu de valeur pour le moment. [ [Portée]]. Pour les fonctions, l'attribut [[Scope]] contient la collection d'objets dans la portée dans laquelle la fonction a été créée - la chaîne de portée. Lorsqu'une fonction est créée dans l'environnement global, la chaîne de portée de la fonction insère un objet global qui contient toutes les variables définies dans la portée globale.
La portée intérieure peut accéder à la portée extérieure, mais la portée extérieure ne peut pas accéder à la portée intérieure. Étant donné que JavaScript n'a pas de portée au niveau du bloc, les variables définies dans une instruction if ou une instruction de boucle for sont accessibles en dehors de l'instruction. Avant ES6, JavaScript n'avait qu'une portée globale et une portée de fonction. ES6 ajoutait un nouveau mécanisme de portée au niveau du bloc.
Lorsque la fonction est exécutée, un objet interne appelé environnement d'exécution (contexte d'exécution, également appelé contexte d'exécution) sera créé pour la fonction d'exécution. Chaque environnement d'exécution possède sa propre chaîne de portées. Lorsqu'un environnement d'exécution est créé, le haut de sa chaîne de portées est d'abord initialisé avec l'objet dans l'attribut [[Scope]] de la fonction en cours d'exécution. Immédiatement après, l'objet actif (y compris toutes les variables locales, les paramètres nommés, le jeu de paramètres d'arguments et ceci) lorsque la fonction est en cours d'exécution sera également créé et poussé vers le haut de la chaîne de portée.
L'environnement d'exécution correspondant est unique à chaque fois qu'une fonction est exécutée. Appeler plusieurs fois la même fonction entraînera la création de plusieurs environnements d'exécution. Une fois l'exécution de la fonction terminée, l'environnement d'exécution sera détruit. Lorsque l'environnement d'exécution est détruit, l'objet actif est également détruit (l'environnement d'exécution global ne sera détruit qu'après la fermeture de l'application, comme la fermeture de la page Web ou du navigateur) .
Pendant l'exécution de la fonction, chaque fois qu'une variable est rencontrée, elle passera par un processus d'analyse d'identifiant pour déterminer où obtenir ou stocker les données. La résolution d'identifiants est le processus de recherche d'identifiants niveau par niveau le long de la chaîne de portée. La variable globale est toujours le dernier objet (c'est-à-dire l'objet fenêtre) de la chaîne de portée.
En JavaScript, il existe deux instructions qui peuvent modifier temporairement la chaîne de portée pendant l'exécution. La première est la déclaration with. L'instruction with crée un objet mutable qui contient toutes les propriétés de l'objet spécifié par le paramètre et pousse l'objet vers la première position de la chaîne de portée, ce qui signifie que l'objet actif de la fonction est compressé vers la deuxième position de la chaîne de portée. chaîne de portée. Bien que cela rende l'accès aux propriétés des objets mutables très rapide, l'accès aux variables locales, etc. devient plus lent. La deuxième instruction qui peut modifier la chaîne de portée de l'environnement d'exécution est la clause catch dans l'instruction try-catch. Lorsqu'une erreur se produit dans le bloc de code try, le processus d'exécution passera automatiquement à la clause catch, puis l'objet d'exception sera poussé dans un objet variable et placé en haut de la portée. À l’intérieur du bloc catch, toutes les variables locales de la fonction seront placées dans le deuxième objet de chaîne de portée. Une fois la clause catch exécutée, la chaîne de portée revient à son état précédent.
Le constructeur en JavaScript peut être utilisé pour créer des objets de types spécifiques. Afin de les distinguer des autres fonctions, les constructeurs commencent généralement par une majuscule. Cependant, cela n'est pas nécessaire en JavaScript, car JavaScript n'a pas de syntaxe spéciale pour définir les constructeurs. En JavaScript, la seule différence entre les constructeurs et les autres fonctions réside dans la manière dont ils sont appelés. N'importe quelle fonction peut être utilisée comme constructeur à condition qu'elle soit appelée via l'opérateur new. La fonction
JavaScript a quatre modes d'appel : 1. Mode d'appel de fonction indépendant, tel que foo(arg). 2. Mode d'appel de méthode, tel que obj.foo(arg). 3. Mode d'appel du constructeur, tel que new foo(arg). 4.call/apply mode d'appel, tel que foo.call(this,arg1,arg2) ou foo.apply(this,args) (args voici un tableau).
Pour créer une instance du constructeur et jouer le rôle du constructeur, vous devez utiliser l'opérateur new. Lorsque nous utilisons l'opérateur new pour instancier un constructeur, les étapes suivantes sont effectuées à l'intérieur du constructeur :
1. Créez implicitement un objet this vide
2. Exécutez le code dans le constructeur (ajoutez des attributs à l'objet this actuel)
3. Renvoie implicitement l'objet this actuel
Si le constructeur renvoie explicitement un objet, alors l'instance est l'objet retourné, sinon c'est l'objet this retourné implicitement.
Lorsque nous appelons le constructeur pour créer une instance, l'instance aura tous les attributs et méthodes d'instance du constructeur. Pour différentes instances créées via des constructeurs, leurs propriétés et méthodes d'instance sont indépendantes. Même s'il s'agit de valeurs de type référence portant le même nom, les différentes instances ne s'affecteront pas.
Le prototype et la chaîne de prototypes ne sont pas seulement l'essence du langage JavaScript, mais aussi l'une des difficultés de ce langage. Le prototype de prototype (prototype explicite) est un attribut unique d'une fonction Chaque fois qu'une fonction est créée, la fonction crée automatiquement un attribut de prototype et pointe vers l'objet prototype de la fonction. Tous les objets prototypes obtiendront automatiquement un attribut constructor (constructeur, qui peut également être traduit par constructeur) Cet attribut contient un pointeur vers la fonction (c'est-à-dire le constructeur lui-même) où. l'attribut prototype est localisé. Lorsque nous créons une instance via le constructeur, l'instance contiendra une propriété interne de [[Prototype]] (prototype implicite), qui pointe également vers l'objet prototype du constructeur. Dans Firefox, Safari et Chrome, chaque objet peut accéder à ses propriétés [[Prototype]] via l'attribut __proto__. Pour les autres navigateurs, cet attribut est totalement invisible pour les scripts.
L'attribut prototype du constructeur et le [[Prototype]] de l'instance pointent tous deux vers l'objet prototype du constructeur. Il n'y a pas de relation directe entre l'attribut [[Prototype]]. de l'instance et du constructeur. Pour savoir si la propriété [[Prototype]] d'une instance pointe vers l'objet prototype d'un certain constructeur, nous pouvons utiliser la méthode isPrototypeOf() ou Object.getPrototypeOf().
Chaque fois qu'une propriété d'une instance d'objet est lue, une recherche est effectuée, ciblant la propriété portant le nom donné. La recherche commence d'abord à partir de l'instance d'objet elle-même. Si un attribut avec un nom donné est trouvé dans l'instance, la valeur de l'attribut est renvoyée ; s'il n'est pas trouvé, la recherche continue pour l'objet prototype pointé par le [[Prototype] ; ] attribut de l'objet. Dans le prototype Recherche dans un objet une propriété portant un nom donné et renvoie la valeur de la propriété si elle est trouvée.
Pour déterminer de quel constructeur l'objet est une instance directe, vous pouvez accéder à la propriété du constructeur directement sur l'instance. L'instance lira la propriété du constructeur sur l'objet prototype via [[Prototype]. ] et renvoie le constructeur lui-même.
Les valeurs de l'objet prototype sont accessibles via l'instance d'objet, mais elles ne peuvent pas être modifiées via l'instance d'objet. Si nous ajoutons une propriété dans l'instance avec le même nom que l'objet prototype de l'instance, alors nous créons la propriété dans l'instance. Cette propriété d'instance nous empêchera d'accéder à cette propriété dans l'objet prototype, mais elle ne modifiera pas cette propriété. Le simple fait de définir la propriété d'instance sur null ne restaure pas l'accès à la propriété dans l'objet prototype. Pour restaurer l'accès à la propriété dans l'objet prototype, vous pouvez utiliser l'opérateur delete pour supprimer complètement la propriété de l'instance d'objet.
Utilisez la méthode hasOwnProperty() pour détecter si une propriété existe dans l'instance ou dans le prototype. Cette méthode renverra true uniquement si la propriété donnée existe dans l'instance d'objet. Pour obtenir toutes les propriétés d'instance énumérables de l'objet lui-même, vous pouvez utiliser la méthode ES5 Object.keys(). Pour obtenir toutes les propriétés d'instance, qu'elles soient énumérables ou non, vous pouvez utiliser la méthode Object.getOwnPropertyNames().
Le prototype est dynamique et toute modification apportée à l'objet prototype peut être immédiatement reflétée à partir de l'instance, mais si l'intégralité de l'objet prototype est réécrite, la situation est différente. L'appel du constructeur ajoutera un pointeur [[Prototype]] vers l'objet prototype d'origine à l'instance d'objet. Après avoir réécrit l'intégralité de l'objet prototype, le constructeur pointe vers le nouvel objet prototype. Toutes les propriétés et méthodes de l'objet prototype existent avec le nouveau prototype. sur l'objet ; et l'instance de l'objet pointe également vers l'objet prototype d'origine, de sorte que la connexion entre le constructeur et l'objet prototype d'origine pointant vers le même objet prototype est rompue, car ils pointent respectivement vers des objets prototypes différents.
Pour restaurer cette connexion, vous pouvez instancier l'instance d'objet après la réécriture du prototype du constructeur, ou modifier l'attribut __proto__ de l'instance d'objet pour pointer vers le nouvel objet prototype du constructeur.
JavaScript utilise la chaîne de prototypes comme principal moyen d'implémenter l'héritage. Il utilise des prototypes pour permettre à un type de référence d'hériter des propriétés et des méthodes d'un autre type de référence. L'instance du constructeur a un attribut [[Prototype]] pointant vers l'objet prototype. Lorsque nous rendons l'objet prototype du constructeur égal à une instance d'un autre type, l'objet prototype contiendra également un pointeur [[Prototype]] vers l'autre prototype. Si un autre prototype est une instance d'un autre type... et ainsi de suite, une chaîne d'instances et de prototypes est formée. C'est le concept de base de ce qu'on appelle la chaîne de prototypes.
La chaîne de prototypes étend le mécanisme de recherche de prototype lors de la lecture d'un attribut d'instance, l'attribut sera d'abord recherché dans l'instance. Si l'attribut n'est pas trouvé, la recherche de l'objet prototype pointé par l'instance [[Prototype]] continuera. L'objet prototype deviendra également une instance d'un autre constructeur. Si l'objet prototype n'est pas trouvé, la recherche continuera. . L'objet prototype [[Prototype]] pointe vers un autre objet prototype... Le processus de recherche continue de rechercher vers le haut le long de la chaîne de prototypes. Si l'attribut ou la méthode spécifié ne peut pas être trouvé, le processus de recherche sera exécuté un par un. chaîne prototype. Elle s’arrêtera à la fin.
Si l'objet prototype de la fonction n'est pas modifié, tous les types référence ont un attribut [[Prototype]] qui pointe vers l'objet prototype d'Object par défaut. Par conséquent, le prototype par défaut de toutes les fonctions est une instance de Object, ce qui constitue la raison fondamentale pour laquelle tous les types personnalisés héritent des méthodes par défaut telles que toString() et valueOf(). Vous pouvez utiliser l'opérateur instanceof ou la méthode isPrototypeOf() pour déterminer si un prototype de constructeur existe dans la chaîne de prototypes de l'instance.
Bien que la chaîne prototype soit très puissante, elle présente également quelques problèmes. Le premier problème est que la valeur du type de référence sur l'objet prototype est partagée par toutes les instances, ce qui signifie que les propriétés ou méthodes du type de référence des différentes instances pointent vers la même mémoire tas. La modification de la valeur de référence sur le prototype d'une instance affectera. toutes les autres instances en même temps. La valeur de référence de l'instance sur l'objet prototype, c'est pourquoi les propriétés ou méthodes privées sont définies dans le constructeur plutôt que sur le prototype. Le deuxième problème avec la chaîne de prototypes est que lorsque nous assimilons le prototype d'un constructeur à une instance d'un autre constructeur, si nous transmettons des paramètres à un autre constructeur pour définir les valeurs d'attribut à ce moment-là, alors toutes les propriétés basées sur l'original constructeur sera Cet attribut de l'instance se verra attribuer la même valeur en raison de la chaîne de prototypes, et ce n'est parfois pas le résultat que nous souhaitons.
La fermeture est l'une des fonctionnalités les plus puissantes de JavaScript. En JavaScript, la fermeture fait référence à une fonction qui a le droit d'accéder à des variables dans le cadre d'une autre fonction. données hors du périmètre local. Une manière courante de créer une fermeture consiste à créer une fonction dans une autre fonction et à renvoyer cette fonction.
De manière générale, lorsque la fonction est exécutée, l'objet actif local sera détruit, et seule la portée globale sera enregistrée en mémoire. Toutefois, la situation est différente en ce qui concerne les fermetures.
L'attribut [[Scope]] de la fonction de fermeture sera initialisé à la chaîne de portée de la fonction qui l'enveloppe, de sorte que la fermeture contient une référence au même objet que la chaîne de portée de l'environnement d'exécution. De manière générale, l'objet actif de la fonction sera détruit ainsi que l'environnement d'exécution. Mais lorsqu'une fermeture est introduite, l'objet actif de la fonction d'origine ne peut pas être détruit car la référence existe toujours dans l'attribut [[Scope]] de la fermeture. Cela signifie que les fonctions de fermeture nécessitent plus de mémoire que les fonctions de non-fermeture, ce qui entraîne davantage de fuites de mémoire. De plus, lorsqu'une fermeture accède à l'objet actif de la fonction d'emballage d'origine, elle doit d'abord résoudre l'identifiant de son propre objet actif dans la chaîne de portée et trouver la couche supérieure. Par conséquent, la fermeture utilise les variables de la fonction d'emballage d'origine. est également préjudiciable à la performance et a un impact important.
Dans les minuteries, les écouteurs d'événements, les requêtes Ajax, la communication entre fenêtres, les Web Workers ou toute autre tâche asynchrone ou synchrone, tant que vous utilisez des fonctions de rappel, vous utilisez en fait des fermetures.包 Le problème typique de fermeture consiste à utiliser la variable de boucle de sortie du minuteur dans la boucle for :
Ce code, pour les amis qui ne sont pas familiers avec la fermeture de JavaScript, peut le prendre pour acquis 1, 2, 3. Cependant , la situation réelle est que les quatre nombres générés par ce code sont tous 4.
En effet, puisque le timer est un mécanisme de chargement asynchrone, il ne sera exécuté que lorsque la boucle for sera parcourue. Chaque fois que le timer est exécuté, le timer recherche la variable i dans sa portée externe. Depuis la fin de la boucle, la variable i dans la portée externe a été mise à jour à 4, donc les variables i obtenues par les quatre minuteries sont toutes 4, au lieu de notre sortie idéale de 0, 1, 2, 3.
Pour résoudre ce problème, nous pouvons créer une nouvelle portée qui enveloppe la fonction d'exécution immédiate, enregistrer la variable i de la portée externe dans chaque boucle dans la portée nouvellement créée et laisser le minuteur démarrer à partir de la nouvelle portée à chaque fois que nous le pouvons. utilisez la fonction d'exécution immédiate pour créer cette nouvelle portée :
De cette façon, le résultat de l'exécution de la boucle affichera 0, 1, 2, 3 dans l'ordre. Nous pouvons également utiliser à nouveau cette fonction d'exécution immédiate pour la simplifier. un peu, passez directement le paramètre d'action i à la fonction d'exécution immédiate, il n'est donc pas nécessaire d'y attribuer une valeur :
Bien sûr, il n'est pas nécessaire d'utiliser la fonction d'exécution immédiate. Vous pouvez. créez également une fonction non anonyme et elle est exécutée à chaque fois qu'elle boucle, mais cela prendra plus de mémoire pour enregistrer la déclaration de fonction.
Comme il n'existait pas de paramètre de portée au niveau du bloc avant ES6, nous ne pouvons résoudre ce problème qu'en créant manuellement une nouvelle portée. ES6 a commencé à définir la portée au niveau du bloc. Nous pouvons utiliser let pour définir la portée au niveau du bloc :
L'opérateur let créera une portée au niveau du bloc et les variables déclarées par let sont enregistrées dans le niveau de bloc actuel. scope. , donc chaque fonction immédiatement exécutée recherchera à chaque fois les variables de sa portée de bloc actuelle.个LET a également une définition spéciale. Elle fait que la variable n'est pas déclarée une seule fois au cours du cycle. Chaque cycle sera re-déclaré et la valeur de la nouvelle instruction est initialisée avec la valeur de la fin du cycle. la tête de la boucle for :
(Ceci dans la nouvelle fonction flèche dans ES6 est différent, et son pointage dépend de la position de la déclaration de fonction.) Rappelez-vous les quatre modes d'appel de fonction que j'ai mentionnés plus tôt : 1.
Modèle d'appel de fonction indépendant, comme foo(arg). 2.Mode d'appel de méthode objet, tel que obj.foo(arg). 3.Mode d'appel du constructeur, tel que new foo(arg). 4.call/applyMode d'appel, comme foo.call(this) ou foo.apply(this). Pour le mode d'appel de fonction indépendant, en mode non strict, celui-ci pointera par défaut vers l'objet global. En mode strict, cela ne peut pas être lié à l'objet global par défaut, il sera donc lié à undefined.
Pour le mode d'appel de méthode objet, this dans la fonction pointera vers l'objet lui-même qui l'appelle :
Pour le mode d'appel de constructeur, les étapes d'exécution à l'intérieur du constructeur ont été introduites auparavant :
1 . Créez implicitement un objet this vide 2. Exécutez le code dans le constructeur (ajoutez des attributs à l'objet this actuel)
3. Renvoyez implicitement l'objet this actuel
Par conséquent, lors de l'appel d'une fonction à l'aide d'une nouvelle méthode, c'est this. pointeur vers Cet objet est créé implicitement et indépendamment à l'intérieur du constructeur.Toutes les propriétés ou méthodes ajoutées via celui-ci seront éventuellement ajoutées à cet objet vide et renvoyées à l'instance du constructeur.
Pour le mode d'appel call/apply, ceci dans la fonction sera lié au premier paramètre que vous transmettez, comme le montre la figure :
foo.apply() et foo.call() La fonction de changement du point pointé par ceci est la même. La seule différence réside dans le fait que le deuxième paramètre soit passé au format tableau ou au format de paramètre dispersé.
À propos des principes sous-jacents de JavaScript, je vais l'écrire temporairement ici aujourd'hui. Je continuerai à mettre à jour le contenu sur JavaScript à l'avenir. Bienvenue pour continuer à y prêter attention.
【Recommandations associées :
Tutoriel d'apprentissage JavascriptCe qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!