JS的底层小知识(三):作用域链与闭包
前言
在前面两篇文章中,我们对JS的作用域以及预编译有了一个充分的认识和了解,相信大家已经很熟悉了。那么接下来,我将分享一下我对作用域链和闭包认识和了解,希望这可以帮助你深入了解作用域链和闭包!💞
在这之前,我们需要先了解一下预编译创建的执行上下文对象(GO和AO)是存放在哪里的,这就要我们先认识一下调用栈了。
调用栈
调用栈: 就像是乌鸦喝水一样,往瓶子内放入一块一块石头的过程和记录,只不过每一块石头的横截面和瓶子一的横截面一样。每次当你开始一个新的任务或者调用一个函数时,就相当于放一块石头在最上面,任务完成或者函数调用完,就会被弹出调用栈,也就是出栈。看下面的图,我们能清晰的看出,调用栈只有一个口,石头只能从一个口进出。所以调用栈遵循“后进先出”原则,也就是说,最后放入栈中的物品将会是最先被取出的。
但是,瓶子的空间是有限的,如果你放的石头太多了,超过了瓶子能承受的高度(调用栈的容量),这时候就会发生“爆栈”。
所以我们知道调用栈是执行上下文对象(GO和AO)存放的地方,以及每当执行上下文对象(GO和AO)运行完毕后,会被弹出栈,也就是会被销毁掉。
调用栈是用来干嘛的,相信大家也很清楚了,接下来就来看看作用域链吧
作用域链
-
首先,全局作用域和函数作用域在执行前会发生预编译,会创建执行上下文对象(GO和AO)
-
每个执行上下文对象(GO和AO)的变量环境中都会有一个内定的 outer 属性,用于指明该函数的外层作用域是谁
-
而 outer 的指向是根据词法作用域来定的
-
js 引擎在查找变量时,会先在当前作用域中查找,找不到就会根据outer的指向去到外层作用域中查找。层层往上,这种查找的关系链就称为 作用域链
ok ,那我们来看一段代码,看看它的作用域链是怎样的
function bar (){
console.log(a);
}
function foo() {
var a = 100
bar();
}
var a=200
foo();
先通过预编译将每个执行上下文对象(GO和AO)按顺序放入栈中
当执行输出的时候,先从当前执行上下文对象开始,逐级向外层作用域查找,直至全局作用域。如果在全局作用域仍未找到,则报错。而在执行上下文对象中,要先从词法环境中开始,到变量环境中。OK,那就来看看吧
红色的线是outer的指向,而黄色的线是console.log(a)
执行时,查找的路线。这时大家肯定会疑惑了,为什么把bar的outer
和foo的outer
的指向是都是指向全局呢。大家肯定是忘记了我上面说的了,因为outer 的指向是根据词法作用域来定的 ,而词法作用域又怎么来定呢,你看看你肯定又忘了,在讲作用域的时候说过,一个函数或者变量声明在哪里,他就在哪里,不会受到运行时调用位置的影响。
而你看这里,bar()
和foo()
都声明在全局变量中,那么他们的outer
指向的都是全局。没问题吧!!
所以这段代码的输出结果是200,而不是100。
OK,可能还有些hxd没有缓过来,那我们再来一段代码
function bar() {
var myname = 'Tom'
let test1 = 100
if (1) {
let myname = 'Jerry'
console.log(test, myname);
}
}
function foo() {
var myname = '彭于晏'
let test = 2
{
let test = 3
bar()
}
}
var myname = '晟哥'
let test = 1
foo()
思考一下,这输出的是什么???
思考完再继续往下看昂
OK,来看看我的图,这图一画出来不就直接,清清楚楚明明白白了吗
所以这里的输出结果是
1
和Jerry
来到这里,相信大家对作用域链也是了如指掌了吧,那就开始进入很多人都觉得很难理解的闭包吧,不过在我看来,也就一般般吧(哈哈哈哈开玩笑的,大佬别喷)。OK,跟着我的脚步来看一下吧。
首先,来看这样一段代码
function foo() {
var name='大仙'
function bar() {
console.log(count,age)
}
var count = 1
var age =18
return bar
}
var age=20
const baz =foo()
baz();
然后画出调用栈,根据预编译画出其中的执行上下文对象,当foo()
函数调用完,该函数就会被弹出栈,也就是销毁。返回bar()函数,也就是调用bar(),所以会有一个执行上文对象入栈,最后就是下图的样子。
那么输出的结果是什么呢?当然很多人看完代码就知道输出的是1和18。可是为什么呢?
foo()
函数被执行完不就被销毁了??
那就要引出我们的闭包了。
闭包
什么是闭包呢?
-
MDN Web Docs 中说:闭包是指那些能够访问自由变量的函数。
-
《JavaScript高级程序设计》 中说:闭包是一个函数,它有权访问其自身范围内的变量、其外部函数的变量,甚至是外部函数的参数。
这样看,能看懂嘛?要是我自己看书,我肯定是不懂。
来看看我的理解:
根据js词法作用域的规则,内部函数总是能访问外部函数中的变量,当通过调用一个外部函数返回的一个内部函数后,即使外部函数执行已经结束了,但是内部函数引用了外部函数中的变量,该变量也依旧需要被保存在内存中,我们把这些变量的集合叫做闭包
简单来说 就是集合是闭包,什么样的集合呢,当通过调用一个外部函数返回的一个内部函数后,即使外部函数执行已经结束了,但是内部函数引用了外部函数中的变量,这些变量的集合咯,但是外部函数已经被销毁了,那么这些集合存放在哪??
欧克欧克,既然这样,那我们再来画个图吧
销毁之后,会留下一个背包,背包里装着bar()函数需要用到的变量。而这个背包就被称为闭包
闭包的优点
-
实现共有变量(企业的模块开发)
- 在模块化开发中,闭包可以用来创建私有变量,同时通过返回的公共方法来操作这些私有变量,实现数据的封装和保护。这种方式使得不同模块之间不会因为全局变量而产生冲突。
-
做缓存
- 利用闭包可以实现数据的缓存机制。当一个耗时的计算或资源获取操作完成后,可以将其结果存储在闭包中,当下次需要相同结果时,直接从闭包中读取,避免了重复计算或请求。
-
封装模块,防止全局变量污染
- 闭包通过限制变量的作用域,有助于减少全局变量的使用,从而避免了因全局变量滥用导致的命名冲突和数据篡改问题。它允许开发者在不污染全局命名空间的前提下,创建具有独立状态和行为的组件或模块。
闭包的缺点
-
内存泄露(内存可用空间越来越小)
- 由于闭包会维持对外部变量的引用,即使外部函数执行完毕,只要闭包还被引用,这些外部变量就不能被垃圾回收机制回收。在长期运行的程序中,特别是当闭包被大量创建且不再使用时,未被释放的内存会逐渐累积,导致内存泄漏,占用调用栈的内存空间,甚至可能导致“爆栈”。解决这一问题需要开发者谨慎管理闭包的生命周期,确保不再需要的闭包能够适时释放对其捕获变量的引用。
练习
var arr=[];
for (var i = 0; i <10; i++) {
arr[i] =function() {
console.log(i);
}
}
arr.forEach(function(item) {item( )})
你知道这段代码的输出结果吗???肯定有人一眼就觉得好简单,输出结果不就是0,1,2,3,4,5,6,7,8,9
嘛。
no no no 这样就大错特错了。好好思考一下哦
那如果需要利用闭包 的相关知识来将这段代码修改成输出结果为0,1,2,3,4,5,6,7,8,9
呢,又该怎么改???
来来来,大佬们敲出你们的答案
写得不好的地方以及需要修改的地方,欢迎大佬们亲临指正昂💞
转载自:https://juejin.cn/post/7373859431081574411