理解JavaScript作用域链是理解系统如何进行变量存取的关键,也是理解全局对象(Global Object,后文简称GO),执行上下文对象(Activation Object,后文简称AO)等的创建与销毁过程的关键。
[[scope]]
[[scope]]是系统内置的一个属性,用于存放作用域链,此属性不可读取,开发人员无法操控,仅供系统使用。每个独立的作用域(全局作用域,函数作用域)形成时都会创建自己独一无二的[[scope]]属性,里面存放该作用域的作用域链。那作用域链到底是什么呢?请看下文
作用域链
我们以一个函数的创建为例,当函数定义时,生成一个[[scope]]属性,将全局对象GO压入自己的作用域链,如图1所示:
当函数定义时,会将自己所在环境(此时是全局环境)初始化到[[scope]]中,当函数执行的时候,会产生执行上下文,也就是AO,放在[[scope]]的最顶层,这样就行成了作用域链,如图2所示:
执行上下文
如上所述:当函数在执行的时候,就会产生执行上下文,也就是函数执行的环境,伴生一个AO对象,可以理解为这个AO对象就是该函数执行时的环境。当在函数中获取变量值的时候,系统会从顶层依次向下查找,也就是说会先查找AO对象中有没有该变量,有的话直接取值,没有则继续查找,直到查找完整个作用域链。看如下示例:
var str = 'abc' // 全局变量str
function fn(){ // 生成[[scope]]属性,将GO放进该属性的值中
var name = 'fn'
console.log(name) // 'fn' 查找name变量时,先从AO开始查找,找到了,返回'fn'
console.log(str) // 'abc' 查找str的时候,先从AO开始查找,没找到,继续向下查找GO,找到了,返回'abc'
}
fn() // 产生执行上下文AO,AO中包含变量name
// 执行完毕,销毁上下文AO
fn() // 再一次执行,产生新的AO
当函数执行完毕之后,函数的执行上下文就会销毁,回到函数定义的状态(图1)等待下一次继续执行,再次执行的时候,会创建新的执行上下文AO,跟之前的AO没有任何联系。
复杂一点的例子
var str = 'abc'
function a(){
var name = 'aaa'
function b(){
var name = 'bbb'
var sex = 'male'
console.log(name) // 'bbb'
}
b()
console.log(sex) // ReferenceError: sex is not defined
}
a()
其实这个例子也非常简单,大部分人一眼就能看出结果,我把它放在这里仅仅是为了说清楚整个函数执行的流程。
-
函数a定义:
生成[[scope]],存入GO。(ps:本人比较懒,直接copy图1了,哈哈哈)
-
函数a执行:
可能有人不理解,为啥第二步就是执行了,而不是定义函数b呢?这里简单说明一下:当系统执行代码的时候,读到function a(){ ... }这一句会把这行语句理解成一个函数声明,保存a变量,并以后面的函数进行赋值,至于函数里面有什么东西,系统现在是不关心的。因此函数a定义完之后,该执行a()语句了。
产生执行上下文AO,放在GO前面。(ps:同理,copy图2)
-
函数b定义(重点)
这里比较关键了,函数b在定义的时候,先生成[[scope]],然后会将函数a当前的作用域链放入[[scope]]里面作为自己作用域链的初始值。划重点!!!
上图所示,为了区分,将a函数的执行上下文AO表示为aAO,即现在b的初始作用域链包含了a的执行上下文,这也就是b函数能访问a函数里的变量的原因。
-
函数b执行
如你所想,函数b执行时会产生自己的执行上下文,我们称之为bAO(仅仅为了区分),bAO同样会被放在[[scope]]的最顶层。
-
函数b执行完成
销毁bAO,回到b定义时的状态(图5)等待下一次被执行,在上述代码里并没有下一次执行,因此直到a执行完成,b的作用域链保持为图5的状态。
-
函数a执行完成
销毁aAO,伴随着里面定义的函数b也会被销毁,此时回到a函数定义的状态(图4),等待下一次执行。注意:下一次执行时再定义的b函数就跟之前的完全没关系了。
思考
经过上述的过程想必大家会有一个疑问:a函数的执行上下文AO跟b函数作用域链里面保存的AO到底是不是同一个AO呢??答案是肯定的。如下例子可以验证:
function a(){
var num = 100
function b(){
num ++
}
b()
console.log(num) // 101
}
a()
上面这个例子很简单,b函数改变了a函数的变量num,通过之前的解析我们知道,在b函数里面访问num会先查找自己的执行上下文bAO,但是并没有找到,就会顺着作用域链去aAO里面找,现在找到了,并将其值加1。b执行完之后bAO销毁,aAO还在,且里面的num值为101。此时a函数里面再访问num变量,很明显会先在a自己的AO里面查找,找到了,结果值被函数b改了,返回101。由此可见b里面保存的aAO跟a函数的AO确实指向同一个对象。因此,前面的图可以这样表示:
趁热打铁
来看一下下面两道面试题,我只在最后给出答案,分析过程就不列出了,大家可自行画图理解。
第一题:
var glob = 20
function a(){
var name = 'mary'
var age = 21
function b(){
var name = 'bbb'
age = 30
console.log(glob) // (1)
}
b()
function c(){
var glob = 123
console.log(name) // (2)
console.log(age) // (3)
}
c()
}
a()
第二题
var name = 'glob'
function a(){
console.log(name)
}
function b(){
var name = 'bbb'
a()
}
b()
答案:
第一题:(1)20 (2) 'mary' (3) 30
第二题: 'glob'
总结
作用域链是一个函数执行时查找变量的仓库,仓库里面最底层存着全局作用域对象GO,然后往上(ps:这个往上往下只是个人记忆的习惯,并不是准确定义)依次是父级创建的执行上下文AO,然后再是自己的AO,查找变量时会从作用域链的最顶层查找,找到就返回,没找到往下继续找,直到GO里面找完位置。
函数作用域链改变只会在两个阶段:
-
函数定义的时候
会在父级作用域的基础上初始化作用域链
-
函数执行的时候
只会创建AO,并放在作用域链最顶层
最后
自己感觉是说清楚了,大家能不能看明白还不知道,希望各位大家多多在评论区留言,谢谢啦!!后续会持续更新js预编译,闭包等文章,觉得此文对自己有帮助的小伙伴可以点个关注不迷路哟(-_-)
参考
本文思路来源于渡一教育-姬成的JavaScript权威课堂,去观看视频效果更佳哦~