开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 5 天、第 5 篇,点击查看活动详情
书接上回
上一篇JavaScript词法作用域&变量对象中有提到JavaScript执行上下文中的三个重要成员:作用域链、变量对象和this。同样在上一篇中讲解了JavaScript词法作用域决定了执行上下文中访问变量的规则,那么他究竟是如何运作的?今天问哦们就来介绍JavaScript执行上下文的另一个成员:作用域链
作用域链
上一篇中有提到,作用域决定了执行上下文中访问变量的规则,限制访问的范围,执行上下文在作用域决定的访问权下查找变量、函数等,作用域链就是创建函数时产生的作用域被激活后留在执行上下文中的访问路径。
阅读上一篇中我们知道,JavaScript使用的是词法作用域,也就是静态作用域,作用域在函数创建时就已经决定了。我们知道每个javascript函数都是一个对象,对象中有些属性我们可以访问,但有些不可以,这些不可读的属性仅供javascript引擎存取,而[[scope]]就是其中一个。每当函数创建时,会将父级执行上下文的变量(活动)对象逐级保存在[[scope]]中,构成一个寻找作用域中变量的链结构,单它其实也并代表完整的作用域链,它仅是当前作用域下的变量对象的链路。
我们就从一下面这段代码为例
let speak = "I am global";
function father(){
function son(speak){
console.log(speak)
}
son(speak)
}
father();
简单理解一下代码,很容易知道代码的输出结果是"I am global",接下来我们就从函数创建开始分析作用域链 的以运行过程。
当上面的示例代码准备开始执行时,首先创建全局作用域,创建全局执行上下文并入栈,全局作用域中不存在父级上下文,所以这里不讨论他的作用域链,此时全局执行上下文和执行上下文栈分别为
globalContext = {
VO : {
speak: "undefined",
father: reference to function father(){}
}
}
ECStack = [ globalContext];
此时father函数已创建完毕,他的作用域也已经被决定了,会将它的父级执行上下文中的活动变量保存在[[scope]]属性中。
father.[[scope]] = {
globalContext.VO
}
开始执行函数father前,同样会做了一系列准备工作
-
全局上下文已经完成了它活动对象内属性的赋值
globalContext.VO = { speak: "I am global", father: reference to function father(){} } -
创建
father函数执行上下文,复制father对象 [[scope]] 属性作为作域作为作用域链、创建活动变量对象,创建活动变量对象,又将自身的活动变量对象压入作用域链,此时它的作用域链才完整,最后执行上下文入栈fatherContext = { AO :{ son: reference to function son(){} }, Scope : [AO, father.[[scope]]] } ECStack = [ fatherContext, globalContext ]; -
son函数被创建,他的父级执行上下文时globalContext,遍历father函数的作用域链,遇到第一个为fatherContext的活动对象,复制进 [[scope]] 属性的底端,之后遇到了father.[[scope]],遍历它,并且得到了全局变量对象,同样复制它放入最底端,最后他的 [[scope]] 为son.[[scope]] = [ fatherContext.AO, globalContext.VO ]
此时father函数的运行准备工作才完成,开始真正运行代码,运行到执行son函数语句时,同样也会做了一系列准备工作
-
son函数需要传入实参speak,JS便沿着作用域链中的变量(活动)对象,去寻找speak,遇到第一个为fatherContext的活动对象,可以看到它里面没有speak属性,继续往下找,找到了全局变量对象,他里面有,便获取它的值,赋值给son函数的形参 -
创建
son函数执行上下文,复制son对象 [[scope]] 属性作为作域作为作用域链、创建活动变量对象,创建活动变量对象,又将自身的活动变量对象压入作用域链,完整它的作用域链,最后执行上下文入栈sonContext = { AO :{ arguments:{ speak: "I am global", } }, Scope : [AO, son.[[scope]]] } ECStack = [ sonContext, fatherContext, globalContext];
最后完成形参speak的打印,结束函数执行,执行上下文逐级出栈并销毁,完成代码运行
总结
本片细致地分析了JavaScript的作用域链,分析了作作用域如何工作,有了以上的分析,其实我们也就理解了闭包产生的原理,大家可以先简单尝试自己分析一下,下一篇我们就学习执行上下文中的另一个最后一个重要成员——this。最后再打个广告,关注公众号程序猿青空,不定期分享各种优秀文章、学习资源、学习课程,能在未来(因为现在还没啥东西)享受更多福利。