Home > Article > Web Front-end > Front-end Advanced (4): Detailed illustration of scope chain and closure
When I was new to JavaScript, I took a lot of detours in learning closures . This time, I went back to sort out the basic knowledge. It is also a very big challenge to explain closure clearly.
How important is closure? If you are new to the front-end, I cannot tell you intuitively how ubiquitous closures are in actual development, but I can tell you that during front-end interviews, you must ask about closures. Interviewers often use their understanding of closures to determine the basic level of the interviewer. It is conservatively estimated that at least 5 out of 10 front-end interviewers will die on closures.
But why are closures so important, yet so many people still don’t understand it? Is it because everyone is unwilling to learn? It's really not the case, but most of the Chinese articles explaining closures that we found through Therefore, the purpose of this article is to explain the closure clearly and clearly, so that readers can fully understand the closure after reading it, instead of vaguely understanding it. 1. Scope and scope chainBefore explaining the scope chain in detail, I assume that you have roughly understood the following important concepts in JavaScript. These concepts will be very helpful.Object With active objects
variable nameor functionname
(because eval is rarely used in our daily development, so we will not discuss it here).
Procedure
Review Let’s take a look at the
life cycle of the execution context we analyzed in the previous article, as shown below.
Execution context life cycle
The reason why we have this question is because everyone has a misunderstanding about scope and scope chain. As we said above, scope is a set of rules, so what is a scope chain? It is the specific implementation of this set of rules. So this is the relationship between scope and scope chain, I believe everyone should understand it.
We know that when a function is called and activated, it will start to create the corresponding execution context. During the execution context generation process, the variable object, scope chain, and the value of this will be determined respectively. In a previous article we explained variable objects in detail, and here, we will explain scope chains in detail.
The scope chain is composed of a series of variable objects in the current environment and the upper environment. It ensures the orderly access of the current execution environment to variables and functions that meet the access permissions.
In order to help everyone understand the scope chain, let me first illustrate it with an example and the corresponding illustration.
var a = 20; function test() { var b = a + 10; function innerTest() { var c = 10; return b + c; } return innerTest(); } test();
In the above example, the execution context of the global, function test, and function innerTest are created successively. We set their variable objects as VO(global), VO(test), VO(innerTest) respectively. The scope chain of innerTest also contains these three variable objects, so the execution context of innerTest can be expressed as follows.
innerTestEC = { VO: {...}, // 变量对象 scopeChain: [VO(innerTest), VO(test), VO(global)], // 作用域链 this: {} }
Yes, you read that right, we can directly use an array to represent the scope chain. The first item of the array, scopeChain[0], is the front end of the scope chain. The last item in the array is the end of the scope chain, and all ends are global variable objects.
Many people will misunderstand that the current scope and the upper scope have an inclusive relationship, but this is not the case. I think a one-way passage starting from the front end and ending at the end is a more appropriate description. As shown in the picture.
Note that because the variable object becomes active when the execution context enters the execution phase Object, this has been mentioned in the previous article, so AO is used to represent it in the figure. Active Object
Yes, the scope chain is composed of a series of variable objects. We can queryvariable objects in this one-way channel identifier, so that you can access variables in the upper scope.
For those who have a little experience using JavaScript but have never really understood the concept of closures, understanding closures can be seen as a rebirth in a sense. Breaking through the closure bottleneck can greatly increase your skills.
Closures are closely related to scope chains;
Closures are confirmed during function execution.
First throw out the definition of closure straightforwardly: A closure is generated when a function can remember and access the scope it is in (except the global scope), Even if the function is executed outside the current scope.
Simply put, assuming that function A is defined inside function B, and when function A is executed, it accesses the variable object inside function B, then B is a closure Bag.
In Basic Advanced (1), I summarized JavaScript’s garbage collection mechanism. JavaScript has an automatic garbage collection mechanism. Regarding the garbage collection mechanism, there is an important behavior, that is, when a value loses its reference in memory, the garbage collection mechanism will find it according to a special algorithm. And recycle it to free up memory.
And we know that the execution context of a function, after the execution is completed, the life cycle ends, then the execution context of the function will lose its reference. The memory space it occupies will soon be released by the garbage collector. However, the existence of closures will prevent this process.
Let’s take a simple example first.
var fn = null; function foo() { var a = 2; function innnerFoo() { console.log(a); } fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn } function bar() { fn(); // 此处的保留的innerFoo的引用 } foo(); bar(); // 2
In the above example, after foo()
is executed, according to common sense, its execution environment life cycle will end and the occupied memory will be released by the garbage collector. But through fn = innerFoo
, the reference to the function innerFoo is retained and copied to the global variable fn. This behavior causes the variable object of foo to be retained. Therefore, when function fn is executed inside function bar, the retained variable object can still be accessed. So the value of variable a can still be accessed at this moment.
In this way, we can call foo a closure.
The following figure shows the scope chain of closure foo.
We can find it inchr## Check the generation of function call stack and scope chain generated when this code is run in the developer tools of #ome browser. As shown below.
For how to observe closures in chrome, and for more examples of closures, please read the Basic Series (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 ); }
点此查看关于此题的详细解读
关于作用域链的与闭包我就总结完了,虽然我自认为我是说得非常清晰了,但是我知道理解闭包并不是一件简单的事情,所以如果你有什么问题,可以在评论中问我。你也可以带着从别的地方没有看懂的例子在评论中留言。大家一起学习进步。
The above is the detailed content of Front-end Advanced (4): Detailed illustration of scope chain and closure. For more information, please follow other related articles on the PHP Chinese website!