序言
在之前文章我们聊了聊JS中预编译的顶层逻辑,其实就是JS会创建一个调用栈,当V8引擎执行时会创建执行上下文并依次推入到栈底,那么今天我们将之前学习的知识用起来,帮助我们开拓JS中的一座大山---闭包,翻过今天这座山,我们将会在JS的学习中再迈出一大步,那么话不多说让我们借助通俗易懂的代码,彻底掌握这些概念。
一、调用栈
用来管理函数调用关系的一种数据结构
当一个函数执行完毕后,它的执行上下文就会出栈
代码实例1
js
function showName(){
console.log('杰哥');
}
showName()
console.log(myName);
var myName = '王艺杰'
我们先来看上述代码,很简单的我们借助之前学习的调用栈知识能轻松说出答案
杰哥
undefined
代码实例2
js
var a = 2
function add() {
var b = 10
return a + b
}
add()
这道题也很简单 答案就是 12
我们这次来分析一下预编译的顺序,以及调用栈里的执行情况
首先编译器创建全局执行上下文压入栈底后,引擎会给a赋值2,然后执行到6行需要编译器再次创建add执行上下文,识别到了变量b,然后引擎继续执行给b赋值10,在add的作用域中找不到变量a于是去add的词法作用域中找到了a = 2 ,最后返回 a + b。
像这种用来管理函数调用关系的一种数据结构。每当函数被调用时,它的执行上下文会被推入调用栈的底部,表示当前函数的执行。当函数执行完毕后,它的执行上下文就会从栈中弹出,控制权会传递给调用该函数的上一个函数。这个机制确保函数的执行顺序和上下文的正确维护。
调用栈在调试JavaScript代码时非常有用,因为它记录了函数的调用顺序,让您能够跟踪代码的执行流程。
代码实例3
接下来我们再来看一段代码,你能知道这段代码的输出结果是什么吗
js
var a = 2
function add(b,c) {
return b + c
}
function addAll(b,c){
var d = 10
result = add(b,c)
return a + result + d //2+9+10
}
addAll(3,6)
没错,答案就是21,但是很可惜你如果用之前的思路想到了答案,编译器会告诉你你的思路是错误的,因为它并不是这样执行的,那么就让我们来分析一下这段代码吧。首先是分析调用栈里的内容
这里面变量刚开始编译识别到标识符时是会赋值为
undefined
的,但是我这里就直接写引擎执行赋值后的结果了,大家就更看的通俗易懂了些
首先编译器正常编译,引擎执行完a赋值后来到执行addAll函数
,编译器再次进行编译创建一个addAll的执行上下文
并进行标识符识别编译,然后引擎执行来到执行add函数
,编译器再创建一个add的执行上下文
,并将b=3,c=6赋值给形参bc,编译器再次执行add函数,返回一个值给result然后add的执行上下文就结束了,于是
add的执行上下文就执行完毕了接着会被移除调用栈
然后回到addAll的执行上下文中返回一个值为 a + result + d
a在addAll的词法作用域中找到了为2,result = 9 , d在本身的作用域中找到了为 10 所以最后的结果为 21 然后addAll的执行上下文也执行完毕了,接着就是被移除调用栈
最后我们全局的执行上下文也就执行完毕了,最后全局执行上下文也被移除调用栈
调用栈的内存也是有限的
js
function foo(){
console.log('hello');
foo()
}
foo()
这段代码是可以无限循环的 如果我们打开浏览器在浏览器中的控制台运行这段代码,浏览器在运行一段时间后会报错
这是因为JS中调用栈的大小并没有想象中那么大,不同浏览器中调用栈可以存储的执行上下文数量也不同,但是可以确定的是都有一个上限。在我们上面那段代码中,调用栈会不断创建foo的执行上下文,但是并没有foo执行上下文执行完毕出栈,最后导致调用栈的内存被用完,也就出现了这种结果。
二、作用域链
作用域链是通过词法作用域来确定某个作用域的外层作用域的链状关系。它用于查找变量,按照从内到外的顺序查找变量的值。这意味着内部作用域可以访问外部作用域的变量,但外部作用域无法访问内部作用域的变量。 作用域链的概念非常重要,因为它决定了变量的可见性和生命周期。理解作用域链有助于编写清晰且可维护的代码,同时防止变量名冲突。接下来通过通俗易懂的代码实例,让我们开始更加清晰认识作用域链
调用栈内执行上下文的访问
来看下面这段代码 思考下运行结果是什么
js
function bar(){
console.log(myName);
}
function foo(){
var myName = '龙龙'
bar()
}
var myName = '君君'
foo()
其实在每个执行上下文中还有一个属性 outer,在全局执行上下文中outer的值为null,在其他执行上下文中这个outer会指向它的词法作用域中,于是这样就构成了一种从内到外的链状查找关系。
我们现在就可以将上面代码的调用栈以及作用域链画出来
因为bar的作用域中找不到myName,然后只能去bar函数的词法作用域中寻找也就是全局作用域中查找 所以最后打印的结果就是 君君
三、闭包
闭包概念
在JavaScript中,闭包是一个重要的概念。根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量。当内部函数被返回到外部函数之外时,即使外部函数执行结束了,但是内部函数引用了外部函数的变量,这些变量仍然会被保存在内存中。这个现象称为闭包。
掌握闭包
js
function foo(){
var myName = '旭旭'
let test1 = 1
let test2 = 2
var innerBar = {
getName:function(){
console.log(test1);
return myName
},
setName: function(newName){
myName = newName
}
}
return innerBar
}
var bar = foo()
bar.setName('浪哥')
console.log(bar.getName());
我们之前分析的调用栈内的执行上下文执行完毕后被移除调用栈,这句话是没有错的,但是当执行上下文被移除调用栈时,或许还会保留一部份它的"遗产"
,用于被后人使用,以备不时之需,这个东西也就是闭包,那么不多BB我们开始分析这段代码编译器执行时的作用域链情况:
- 首先,在全局作用域中有foo为一个函数,bar变量为undefined,以及一个outer=null
- 然后引擎开始执行bar赋值语句,又得喊来编译器继续编译,创建一个foo执行上下文,我们直接简化,变量环境里面有myName变量值为"旭旭"、innerbar对象和一个 outer,在词法环境中还有 变量test1 = 1 ,test2 = 2,并把innerBar这个对象返回出去赋给变量bar,到这里foo执行上下文就结束了。
- 但是foo执行上下文中还有一些变量被引用出去了,如果自身所有变量都被移除调用栈后续要用就获取不到了,所以在foo执行上下文的旁边还会有一个小背包用于存放引用出去的变量(也就是test1和myName)
- 我们举个通俗易懂的例子你会更好理解,引擎就是一个老板,他雇佣了一个尽职尽责的清洁工,安排他必须 按时清理执行完毕的执行上下文,于是他今天遇到了foo执行上下文,但是foo执行上下文说他也不知道自己到底有没有执行完,于是两人商量着,叫清洁工先把自己执行完的一部份清理出栈,把拿出去的部分保留到内存中,而这个保留引用出去的部分就是闭包。
- 然后引擎接着执行bar对象里的setName函数调用执行,将myName更新为
"朗哥"
- 输出执行getName方法的结果
- 最终执行结果为
闭包的优点和缺点
从上述代码我们了解了闭包可以实现变量的存储,当一个执行上下文被移出调用栈,如果里面有后续可能用到的变量可以用 return 将其返回出该作用域,如果后面要用的话不会拿不到这个值。
变量私有化
在企业里团队框架开发时,都不会把变量声明在全局,因为不能要求所有人的命名都不同,所以都会在写在一个函数作用域中然后引用出去形成闭包,这种操作也被称为变量私有化
内存泄漏
但是闭包也有一些缺点就是内存泄漏,虽然原本的执行上下文被移出栈了,但是形成的闭包仍会占用空间,当闭包太多了也会占满调用栈的内存,所以我们如果不是特定环境不会使用这种方法。
总结
调用栈
-
用来管理函数调用关系的一种数据结构
-
当一个函数执行完毕后,它的执行上下文就会出栈
作用域链
- 通过词法作用域来确定某作用域的外层作用域,查找变量由内而外的这种链状关系,叫做作用域链。
闭包
-
在JS中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量的,当内部函数被返回到外部函数之外时,即使外部函数执行结束了,但是内部函数引用了外部函数的变量,那么这些变量依旧会被保存在内存中,我们把这些变量的集合成为闭包。
-
变量私有化
-
内存泄漏
感谢大家的阅读,点点赞吧♥
如果想了解更多有用的干货,点赞+收藏第一时间获取有用的小知识
开源Git仓库: gitee.com/cheng-bingw...