Maison > Article > interface Web > Front-end Advanced (4) : illustration détaillée de la chaîne de portée et de la fermeture
Quand j'étais nouveau dans l'apprentissage du JavaScript, j'ai fait beaucoup de détours dans l'apprentissage des fermetures . Cette fois, en revenant pour trier les connaissances de base, c'est aussi un très grand défi d'expliquer clairement la clôture.
Quelle est l'importance des fermetures ? Si vous êtes nouveau dans le front-end, je ne peux pas vous dire intuitivement à quel point les fermetures sont omniprésentes dans le développement réel, mais je peux vous dire que vous devez poser des questions sur les fermetures lors des entretiens front-end. Les enquêteurs utilisent souvent leur compréhension des fermetures pour déterminer le niveau de base de l'intervieweur. On estime de manière prudente qu'au moins 5 enquêteurs de première ligne sur 10 mourront à la suite d'une fermeture.
Mais pourquoi les fermetures sont-elles si importantes, alors que tant de gens ne les comprennent toujours pas ? Est-ce parce que tout le monde ne veut pas apprendre ? Ce n’est vraiment pas le cas, mais la plupart des articles chinois expliquant les fermetures que nous avons trouvés grâce à la recherche n’expliquaient pas clairement les fermetures. Soit c'est superficiel, soit c'est incompréhensible, soit c'est tout simplement absurde. Moi y compris, j'ai écrit un jour un résumé sur les fermetures. Avec le recul, je ne pouvais pas supporter de le regarder [le couvre-visage].
Par conséquent, le but de cet article est d'expliquer la clôture de manière claire et claire, afin que les lecteurs puissent pleinement comprendre la clôture après l'avoir lu, au lieu d'une demi-compréhension.
Avant d'expliquer la chaîne de portée en détail, je suppose que vous avez à peu près compris les concepts importants suivants en JavaScript. Ces concepts seront très utiles.
BasiqueType de données et RéférenceType de données
Espace mémoire
Mécanisme de collecte des déchets
Contexte d'exécution
Si vous ne l'avez pas encore compris, vous pouvez lire les trois premiers articles de cette série. Il y a un lien vers une table des matières à la fin de cet article. Afin d'expliquer les fermetures, j'ai préparé les connaissances de base pour chacun. Haha, quel grand spectacle.
Portée
En JavaScript, nous pouvons définir la portée comme un ensemble de règles, qui sont utilisées pour gérer la façon dont le moteur recherche de variables en fonction de nom de l'identifiant dans la portée actuelle et les sous-portées imbriquées.
L'identifiant ici fait référence au nom de la variable ou à la fonction nom
JavaScript Il n'y a que portée globale et portée de la fonction (car eval est rarement utilisé dans notre développement habituel, nous n'en discuterons donc pas ici).
La portée et le contexte d'exécution sont deux concepts complètement différents. Je sais que beaucoup de gens les confondent, mais veillez à bien les distinguer.
L'ensemble du processus d'exécution du code JavaScript est divisé en deux étapes, l'étape de compilation du code et l'étape d'exécution du code. La phase de compilation est complétée par le compilateur, qui traduit le code en code exécutable. Les règles de portée sont déterminées à ce stade. La phase d'exécution est complétée par le moteur. La tâche principale est d'exécuter le code exécutable. Le contexte d'exécution est créé dans cette phase.
Chaîne de portée
Révisons jetez un œil au cycle de vie du contexte d'exécution que nous avons analysé dans l'article précédent, comme indiqué ci-dessous.
Nous avons constaté que la chaîne de portée est générée lors de la phase de création du contexte d'exécution. C'est étrange. Nous venons de dire plus haut que les règles de scope sont déterminées au stade de la compilation, mais pourquoi la chaîne de scope est-elle déterminée au stade de l'exécution ?
La raison pour laquelle nous posons cette question est que tout le monde a un malentendu sur la portée et la chaîne de portée. Comme nous l'avons dit plus haut, la portée est un ensemble de règles, alors qu'est-ce qu'une chaîne de portée ? Il s'agit de la mise en œuvre spécifique de cet ensemble de règles. C'est donc la relation entre la portée et la chaîne de portée, je pense que tout le monde devrait la comprendre.
Nous savons que lorsqu'une fonction est appelée et activée, elle commencera à créer le contexte d'exécution correspondant. Au cours du processus de génération du contexte d'exécution, l'objet variable, la chaîne de portée et la valeur de celui-ci seront déterminés respectivement. . Dans un article précédent, nous avons expliqué en détail les objets variables, et ici, nous expliquerons en détail les chaînes de portée.
La chaîne de portée est composée d'une série d'objets variables dans l'environnement actuel et l'environnement supérieur. Elle garantit l'accès ordonné de l'environnement d'exécution actuel aux variables et fonctions qui répondent aux autorisations d'accès.
Afin d'aider tout le monde à comprendre la chaîne de portée, permettez-moi d'abord de l'illustrer avec un exemple et le schéma correspondant.
var a = 20; function test() { var b = a + 10; function innerTest() { var c = 10; return b + c; } return innerTest(); } test();
Dans l'exemple ci-dessus, le contexte d'exécution du global, de la fonction test et de la fonction innerTest sont créés successivement. Nous définissons respectivement leurs objets variables comme VO(global), VO(test), VO(innerTest). La chaîne de portée de innerTest inclut ces trois objets variables en même temps, de sorte que le contexte d'exécution de innerTest peut être exprimé comme suit.
innerTestEC = { VO: {...}, // 变量对象 scopeChain: [VO(innerTest), VO(test), VO(global)], // 作用域链 this: {} }
Oui, vous avez bien lu, on peut utiliser directement un tableau pour représenter la chaîne de portées. Le premier élément du tableau, scopeChain[0], est la fin de. la chaîne de portées. L'extrémité avant et le dernier élément du tableau sont la fin de la chaîne de portées, et toutes les extrémités sont des objets variables globales.
Beaucoup de gens peuvent mal comprendre que le périmètre actuel et le périmètre supérieur ont une relation inclusive, mais ce n'est pas le cas. Je pense qu'un passage à sens unique commençant à l'avant et se terminant à la fin est une description plus appropriée. Comme le montre l'image.
Notez que parce que l'objet variable devient actif lorsque le contexte d'exécution entre dans la phase d'exécution de l'objet, cela a a été mentionné dans l'article précédent, donc AO est utilisé pour le représenter dans la figure. Objet
Oui, la chaîne de portée est composée d'une série d'objets variables. Nous pouvons interroger l'objet variable dans cet identifiant de canal unidirectionnel. , afin que vous puissiez accéder aux variables dans la portée supérieure.
Pour ceux qui ont un peu d'expérience dans l'utilisation de JavaScript mais qui n'ont jamais vraiment compris le concept des fermetures, comprendre les fermetures peut être vu comme une renaissance dans un sens. les goulots d’étranglement peuvent augmenter considérablement vos compétences.
Les fermetures sont étroitement liées aux chaînes de portée
Les fermetures sont confirmées lors de l'exécution de la fonction.
Jetez d'abord directement la définition de la fermeture : Une fermeture est générée lorsqu'une fonction peut se souvenir et accéder à la portée dans laquelle elle se trouve (à l'exception de la portée globale), même si la la fonction est exécutée en dehors de la portée actuelle.
Pour faire simple, en supposant que la fonction A est définie à l'intérieur de la fonction B, et lorsque la fonction A est exécutée, elle accède à l'objet variable à l'intérieur de la fonction B, alors B est un sac de fermeture.
Dans Basic Advanced (1), j'ai résumé le mécanisme de garbage collection de JavaScript. JavaScript dispose d'un mécanisme de récupération de place automatique. Concernant le mécanisme de récupération de place, il existe un comportement important, c'est-à-dire que lorsqu'une valeur perd sa référence en mémoire, le mécanisme de récupération de place la retrouvera selon un algorithme spécial. . Et recyclez-le pour libérer de la mémoire.
Et on sait qu'après l'exécution du contexte d'exécution de la fonction, le cycle de vie se termine, alors le contexte d'exécution de la fonction perdra sa référence. L'espace mémoire qu'il occupe sera bientôt libéré par le garbage collector. Cependant, l’existence de fermetures empêchera ce processus.
Commençons par un exemple simple.
var fn = null; function foo() { var a = 2; function innnerFoo() { console.log(a); } fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn } function bar() { fn(); // 此处的保留的innerFoo的引用 } foo(); bar(); // 2
Dans l'exemple ci-dessus, après l'exécution de foo()
, selon le bon sens, le cycle de vie de son environnement d'exécution se terminera et la mémoire occupée sera libérée par le garbage collector. Mais grâce à fn = innerFoo
, la référence à la fonction innerFoo est conservée et copiée dans la variable globale fn. Ce comportement entraîne la conservation de l'objet variable de foo. Par conséquent, lorsque la fonction fn est exécutée dans la barre de fonctions, l'objet variable conservé est toujours accessible. La valeur de la variable a est donc toujours accessible à ce moment.
De cette façon, nous pouvons appeler foo une fermeture.
La figure suivante montre la chaîne de portée de la fermeture foo.
Nous pouvons le trouver dans chrAffichez la pile d'appels de fonction et la génération de chaîne de portée générée lorsque ce code est exécuté dans les outils de développement du navigateur Ome. Comme indiqué ci-dessous.
Pour plus d'informations sur la façon d'observer les fermetures dans Chrome et pour plus d'exemples de fermetures, veuillez lire la série de base (6)
在上面的图中,红色箭头所指的正是闭包。其中Call Stack为当前的函数调用栈,Scope为当前正在被执行的函数的作用域链,Local为当前的局部变量。
所以,通过闭包,我们可以在其他的执行上下文中,访问到函数的内部变量。比如在上面的例子中,我们在函数bar的执行环境中访问到了函数foo的a变量。个人认为,从应用层面,这是闭包最重要的特性。利用这个特性,我们可以实现很多有意思的东西。
不过读者老爷们需要注意的是,虽然例子中的闭包被保存在了全局变量中,但是闭包的作用域链并不会发生任何改变。在闭包中,能访问到的变量,仍然是作用域链上能够查询到的变量。
对上面的例子稍作修改,如果我们在函数bar中声明一个变量c,并在闭包fn中试图访问该变量,运行结果会抛出错误。
var fn = null; function foo() { var a = 2; function innnerFoo() { console.log(c); // 在这里,试图访问函数bar中的c变量,会抛出错误 console.log(a); } fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn } function bar() { var c = 100; fn(); // 此处的保留的innerFoo的引用 } foo(); bar();
关于这一点,很多同学把函数调用栈与作用域链没有分清楚,所以有的大神看了我关于介绍执行上下文的文章时就义正言辞的说我的例子有问题,而这些评论有很大的误导作用,为了帮助大家自己拥有能够辨别的能力,所以我写了基础(六),教大家如何在chrome中观察闭包,作用域链,this等。当然我也不敢100%保证我文中的例子就一定正确,所以教大家如何去辨认我认为才是最重要的。
闭包的应用场景
接下来,我们来总结下,闭包的常用场景。
我们知道setTimeout的第一个参数是一个函数,第二个参数则是延迟的时间。在下面例子中,
function fn() { console.log('this is test.') } var timer = setTimeout(fn, 1000); console.log(timer);
执行上面的代码,变量timer的值,会立即输出出来,表示setTimeout这个函数本身已经执行完毕了。但是一秒钟之后,fn才会被执行。这是为什么?
按道理来说,既然fn被作为参数传入了setTimeout中,那么fn将会被保存在setTimeout变量对象中,setTimeout执行完毕之后,它的变量对象也就不存在了。可是事实上并不是这样。至少在这一秒钟的事件里,它仍然是存在的。这正是因为闭包。
很显然,这是在函数的内部实现中,setTimeout通过特殊的方式,保留了fn的引用,让setTimeout的变量对象,并没有在其执行完毕后被垃圾收集器回收。因此setTimeout执行结束后一秒,我们任然能够执行fn函数。
柯里化
在函数式编程中,利用闭包能够实现很多炫酷的功能,柯里化算是其中一种。关于柯里化,我会在以后详解函数式编程的时候仔细总结。
模块
在我看来,模块是闭包最强大的一个应用场景。如果你是初学者,对于模块的了解可以暂时不用放在心上,因为理解模块需要更多的基础知识。但是如果你已经有了很多JavaScript的使用经验,在彻底了解了闭包之后,不妨借助本文介绍的作用域链与闭包的思路,重新理一理关于模块的知识。这对于我们理解各种各样的设计模式具有莫大的帮助。
(function () { var a = 10; var b = 20; function add(num1, num2) { var num1 = !!num1 ? num1 : a; var num2 = !!num2 ? num2 : b; return num1 + num2; } window.add = add; })(); add(10, 20);
在上面的例子中,我使用函数自执行的方式,创建了一个模块。add是模块对外暴露的一个公共方法。而变量a,b被作为私有变量。在面向对象的开发中,我们常常需要考虑是将变量作为私有变量,还是放在构造函数中的this中,因此理解闭包,以及原型链是一个非常重要的事情。模块十分重要,因此我会在以后的文章专门介绍,这里就暂时不多说啦。
为了验证自己有没有搞懂作用域链与闭包,这里留下一个经典的思考题,常常也会在面试中被问到。
利用闭包,修改下面的代码,让循环输出的结果依次为1, 2, 3, 4, 5
for (var i=1; i<=5; i++) { setTimeout( function timer() { console.log(i); }, i*1000 ); }
点此查看关于此题的详细解读
关于作用域链的与闭包我就总结完了,虽然我自认为我是说得非常清晰了,但是我知道理解闭包并不是一件简单的事情,所以如果你有什么问题,可以在评论中问我。你也可以带着从别的地方没有看懂的例子在评论中留言。大家一起学习进步。
Ce 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!